创建的Subject实例,必须要绑定到当前线程上,当执行完毕,需要解除绑定。现在流行的测试框架像JUnit,TestNG都支持"setup"和"teardown"。我们可以利用这个特性模拟Shiro在应用中的完整操作。我们创建了AbstractShiroTest抽象类,可用于单元测试和集成测试:
import org.apache.shiro.SecurityUtils; import org.apache.shiro.UnavailableSecurityManagerException; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; import org.apache.shiro.util.LifecycleUtils; import org.apache.shiro.util.ThreadState; import org.junit.AfterClass; /** * Abstract test case enabling Shiro in test environments. */ public abstract class AbstractShiroTest { private static ThreadState subjectThreadState; public AbstractShiroTest() { } /** * Allows subclasses to set the currently executing {@link Subject} instance. * * @param subject the Subject instance */ protected void setSubject(Subject subject) { clearSubject(); subjectThreadState = createThreadState(subject); subjectThreadState.bind(); } protected Subject getSubject() { return SecurityUtils.getSubject(); } protected ThreadState createThreadState(Subject subject) { return new SubjectThreadState(subject); } /** * Clears Shiro's thread state, ensuring the thread remains clean for future test execution. */ protected void clearSubject() { doClearSubject(); } private static void doClearSubject() { if (subjectThreadState != null) { subjectThreadState.clear(); subjectThreadState = null; } } protected static void setSecurityManager(SecurityManager securityManager) { SecurityUtils.setSecurityManager(securityManager); } protected static SecurityManager getSecurityManager() { return SecurityUtils.getSecurityManager(); } @AfterClass public static void tearDownShiro() { doClearSubject(); try { SecurityManager securityManager = getSecurityManager(); LifecycleUtils.destroy(securityManager); } catch (UnavailableSecurityManagerException e) { //we don't care about this when cleaning up the test environment //(for example, maybe the subclass is a unit test and it didn't // need a SecurityManager instance because it was using only // mock Subject instances) } setSecurityManager(null); } }
2. Unit Testing
由于单元测试测试的是业务逻辑,那么就可以使用EasyMock和Mockito对逻辑依赖的Shiro API进行mock。Shiro对此有很好的支持-可以模拟Subject实例,只需要注意,Subject实例要与当前线程绑定。下面这个例子使用了EasyMock:
import org.apache.shiro.subject.Subject; import org.junit.After; import org.junit.Test; import static org.easymock.EasyMock.*; /** * Simple example test class showing how one may perform unit tests for code that requires Shiro APIs. */ public class ExampleShiroUnitTest extends AbstractShiroTest { @Test public void testSimple() { //1. Create a mock authenticated Subject instance for the test to run: Subject subjectUnderTest = createNiceMock(Subject.class); expect(subjectUnderTest.isAuthenticated()).andReturn(true); //2. Bind the subject to the current thread: setSubject(subjectUnderTest); //perform test logic here. Any call to //SecurityUtils.getSubject() directly (or nested in the //call stack) will work properly. } @After public void tearDownSubject() { //3. Unbind the subject from the current thread: clearSubject(); } }
可以看出,我们没有初始化SecurityManager和Realm实例,只是通过setSubject方法将模拟的Subject实例绑定到当前线程上。这就保证了测试时,没有任何问题。需要注意,setSubject将模拟的Subject对象绑定到当前线程,当再次以不同的Subject参数调用setSubject方法或执行clearSubject()方法时,才解除Subject与线程的绑定。
3. Integration Testing
也可以很容易地在Shiro里做集成测试。SecurityManager实例和其封装的组件,都是轻量级的POJO,占用很少的内存。这意味着,可以在每次测试时,创建和销毁SecurityManager实例。执行集成测试时,可以使用真实的SecurityManager和Subject实例:
import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.junit.After; import org.junit.BeforeClass; import org.junit.Test; public class ExampleShiroIntegrationTest extends AbstractShiroTest { @BeforeClass public static void beforeClass() { //0. Build and set the SecurityManager used to build Subject instances used in your tests // This typically only needs to be done once per class if your shiro.ini doesn't change, // otherwise, you'll need to do this logic in each test that is different Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:test.shiro.ini"); setSecurityManager(factory.getInstance()); } @Test public void testSimple() { //1. Build the Subject instance for the test to run: Subject subjectUnderTest = new Subject.Builder(getSecurityManager()).buildSubject(); //2. Bind the subject to the current thread: setSubject(subjectUnderTest); //perform test logic here. Any call to //SecurityUtils.getSubject() directly (or nested in the //call stack) will work properly. } @AfterClass public void tearDownSubject() { //3. Unbind the subject from the current thread: clearSubject(); } }
可以看出,集成测试和单元测试有不一样的地方:
1. 通过@BeforeClass创建了真实的SecurityManager实例。
2. @Test方法里也创建了真实的Subject,并绑定到线程里。