使用Apache Shiro进行测试

这部分文档解释了如何在单元测试中启用Shiro。

测试须知

正如我们在Subject引用中已经介绍过的,我们知道Subject是“当前执行”用户的安全特定视图,并且Subject实例总是绑定到线程,以确保我们知道在线程执行期间的任何时候谁在执行逻辑。

这意味着,为了能够访问当前正在执行的主题,必须总是发生三件基本的事情:

  1. 必须创建一个Subject实例

  2. Subject实例必须绑定到当前执行的线程。

  3. 在线程完成执行之后(或者如果线程的执行导致一个Throwable),必须解除Subject的绑定,以确保线程在任何线程池环境中保持“干净”。

Shiro有一些架构组件,可以为正在运行的应用程序自动执行这种绑定/解绑定逻辑。

例如,在web应用程序中,根Shiro过滤器在过滤请求时执行此逻辑。

但是由于测试环境和框架的不同,我们需要自己为我们所选择的测试框架执行这个绑定/解绑定逻辑。

测试设置

因此,我们知道在创建Subject实例后,它必须绑定到thread。在线程(或者在本例中是一个测试)完成执行后,我们必须解绑定Subject以保持线程“干净”。

幸运的是,像JUnit和TestNG这样的现代测试框架本身就支持这种“设置”和“拆除”的概念。

我们可以利用这种支持来模拟Shiro在一个“完整”的应用程序中会做什么。

我们已经创建了一个基抽象类,您可以在下面的测试中使用它——您可以根据需要随意复制和/或修改它。

它既可以用于单元测试,也可以用于集成测试(本例中我们使用了JUnit,但TestNG也可以):

  • 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);
    }
}

测试与框架

AbstractShiroTest类中的代码使用Shiro的ThreadState概念和一个静态安全管理器。

这些技术在测试和框架代码中很有用,但很少在应用程序代码中使用。

大多数使用Shiro的终端用户需要确保线程状态的一致性,他们几乎总是使用Shiro的自动管理机制,即“主题”。

与‘and the’主语联系起来。执行的方法。这些方法在主题线程关联参考中有介绍)。

单元测试

单元测试主要是测试你的代码,并且只在有限的范围内测试你的代码。

当你考虑到Shiro时,你真正想要关注的是你的代码能够正确地与Shiro的API一起工作——你不需要测试Shiro的实现是否能够正确地工作(这是Shiro开发团队必须在Shiro的代码库中确保的事情)。

测试Shiro的实现是否与您的实现一起工作,这实际上是集成测试(下面将讨论)。

ExampleShiroUnitTest

因为单元测试更适合测试您自己的逻辑(而不是您的逻辑可能调用的任何实现),所以mock逻辑所依赖的任何api是一个好主意。

这在Shiro上工作得很好——你可以模仿Subject接口,让它反映你想让测试代码响应的任何条件。

我们可以利用像EasyMock和Mockito这样的现代模拟框架来为我们完成这项工作。

但如上所述,Shiro测试中的关键是记住,在测试执行期间,任何Subject实例(模拟的或真实的)都必须绑定到线程。

因此,我们所需要做的就是绑定mock主题,以确保一切按预期工作。

(这个例子使用了EasyMock,但是Mockito也一样):

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();
    }

}

如您所见,我们没有设置Shiro SecurityManager实例或配置一个域或类似的任何东西。

我们只是创建一个mock Subject实例,并通过setSubject方法调用将其绑定到线程。

这将确保在我们的测试代码中或正在测试的代码中对 SecurityyUtils.getsubject() 的任何调用都能正确工作。

请注意,setSubject方法实现将mock Subject绑定到线程,直到使用不同的Subject实例调用setSubject或通过clearSubject()调用显式地将其从线程中清除为止。

将主题绑定到线程多长时间(或在不同的测试中将其交换为一个新实例)取决于您和您的测试需求。

tearDownSubject()

本例中的tearDownSubject()方法使用了Junit 4注释,以确保在每个测试方法执行之后,主题都从线程中清除,不管是什么。

这需要您设置一个新的Subject实例,并(通过setSubject)为每个执行的测试设置它。

然而,这并不是绝对必要的。例如,您可以在每个测试的开始(比如在@ before注释的方法中)绑定一个新的Subject实例(通过setSujbect)。但如果要这样做,不妨使用@After tearDownSubject()方法来保持对称和“干净”。

您可以在每个方法中手动混合和匹配这种设置/删除逻辑,或者根据需要使用@Before和@After注释。

然而,AbstractShiroTest超类将在所有测试之后解除主题与线程的绑定,因为它的tearDownShiro()方法中有@AfterClass注释。

小结

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次相遇。

参考资料

10 Minute Tutorial on Apache Shiro

https://shiro.apache.org/reference.html

https://shiro.apache.org/session-management.html