使用 Apache Shiro 进行测试

文档的此部分说明了如何在单元测试中启用 Shiro。

测试须知

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

这意味着必须始终发生三件事,以支持能够访问当前正在执行的 Subject:

  • 必须创建一个Subject实例

  • Subject实例必须“绑定”到当前正在执行的线程。

  • 线程完成执行之后(或者如果线程的执行结果为Throwable),则Subject必须为“未绑定” *,以确保线程在任何线程池化的环境中保持“干净”。

Shiro 的体系结构组件可为正在运行的应用程序自动执行此绑定/取消绑定逻辑。例如,在 Web 应用程序中,根 Shiro 过滤器在过滤请求时执行此逻辑。但是,由于测试环境和框架不同,我们需要针对所选测试框架自己执行此绑定/解除绑定逻辑。

Test Setup

因此,我们知道在创建Subject实例后,必须将其绑定到线程。在线程(或本例中为测试)完成执行之后,我们必须* unbind * 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);
    }
}

Testing & Frameworks

AbstractShiroTest类中的代码使用 Shiro 的ThreadState概念和静态 SecurityManager。这些技术在测试和框架代码中很有用,但很少在应用程序代码中使用。

大多数需要确保线程状态一致性的与 Shiro 合作的最终用户,几乎都会使用 Shiro 的自动 Management 机制,即 Subject.associateWith 和 Subject.execute 方法。这些方法在主题线程关联的参考中介绍。

Unit Testing

单元测试主要是关于测试您的代码,而仅是在有限范围内的代码。考虑到 Shiro 时,您 true 要关注的是您的代码可以与 Shiro 的* API *一起正常工作-您并不需要测试 Shiro 的实现是否正常工作(这是 Shiro 开发团队必须确保的事情)在 Shiro 的代码库中)。

检验 Shiro 的实现是否可以与您的实现一起工作的测试实际上是集成测试(下面讨论)。

ExampleShiroUnitTest

因为单元测试更适合测试您自己的逻辑(而不是您的逻辑可能调用的任何实现),所以“模拟”您逻辑所依赖的任何 API 是一个好主意。这在 Shiro 上非常有效-您可以模拟Subject接口,并使其反映您希望被测代码对之做出反应的任何条件。我们可以利用诸如EasyMockMockito之类的现代模拟框架为我们完成此任务。

但是如上所述,Shiro 测试中的关键是要记住在测试执行期间必须将任何 Subject 实例(模拟或真实)绑定到线程。因此,我们要做的就是绑定模拟主题,以确保事情按预期进行。

(此示例使用 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实例或配置Realm或类似的东西。我们只是在创建一个模拟Subject实例,并通过setSubject方法调用将其绑定到线程。这样可以确保测试代码或我们正在测试的SecurityUtils.getSubject()代码中的所有调用均能正常工作。

请注意,setSubject方法实现会将您的模拟 Subject 绑定到线程,并且将保留在那里,直到您使用其他Subject实例调用setSubject或通过clearSubject()调用从线程中明确清除它为止。

保持主题与线程绑定多长时间(或将其替换为其他测试中的新实例)取决于您和您的测试要求。

tearDownSubject()

该示例中的tearDownSubject()方法使用 Junit 4 注解,以确保无论执行哪种测试方法,在执行线程后都会从线程中清除 Subject。这要求您设置一个新的Subject实例,并为每个执行的测试(通过setSubject)进行设置。

但是,这并非绝对必要。例如,您可以在每个测试的开始(例如,以@BeforeComments 的方法)(通过setSujbect)绑定一个新的 Subject 实例。但是,如果要执行此操作,则最好使用@After tearDownSubject()方法保持事物对称和“干净”。

您可以在每种方法中手动混合和匹配此设置/拆卸逻辑,或者使用@Before 和@AfterComments(如果您认为合适)。但是,由于所有测试中的tearDownShiro()方法中都有@AfterClass注解,因此AbstractShiroTest超类将在所有测试后将其从线程中解除绑定。

Integration Testing

既然我们已经介绍了单元测试设置,那么让我们来谈谈集成测试。集成测试是跨 API 边界测试实现。例如,在调用实现 B 时测试实现 A 是否有效,而实现 B 则按预期进行测试。

您也可以在 Shiro 中轻松执行集成测试。 Shiro 的SecurityManager实例及其包装的东西(例如 Realms 和 SessionManager 等)都是非常轻量级的 POJO,它们使用的内存很少。这意味着您可以为执行的每个测试类创建和拆除SecurityManager实例。当您的集成测试运行时,它们将使用“真实” SecurityManagerSubject实例,就像您的应用程序将在运行时使用一样。

ExampleShiroIntegrationTest

下面的示例代码看起来与上面的单元测试示例几乎相同,但是三步过程略有不同:

  • 现在有一个步骤“ 0”,它设置了一个“真实的” SecurityManager 实例。

  • 现在,第 1 步使用Subject.Builder构造一个“真实”主题实例,并将其绑定到线程。

线程绑定和取消绑定(步骤 2 和 3)的功能与单元测试示例相同。

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

如您所见,实例SecurityManager的具体实现已实例化,并可以通过setSecurityManager方法在其余的测试中访问。然后,稍后通过getSecurityManager()方法使用Subject.Builder时,测试方法可以使用此SecurityManager

还要注意,SecurityManager实例是通过@BeforeClass设置方法设置的,这对于大多数测试类来说是相当普遍的做法。但是,如果愿意,您可以创建一个新的SecurityManager实例,并通过任何测试方法随时通过setSecurityManager对其进行设置-例如,您可以根据测试要求引用两个不同的.ini 文件来构建新的SecurityManager

最后,就像单元测试示例一样,AbstractShiroTest超级类将通过其@AfterClass tearDownShiro()方法清除所有 Shiro 构件(所有剩余的SecurityManagerSubject实例),以确保线程“干净”以供下一个测试类运行。

协助处理文档

尽管我们希望该文档对您使用 Apache Shiro 所做的工作有所帮助,但社区一直在不断改进和扩展文档。如果您想为 Shiro 项目提供帮助,请考虑在需要的地方更正,扩展或添加文档。您提供的每一点帮助都会扩大社区,进而改善 Shiro。

提交文档的最简单方法是通过单击下面的Edit链接提交请求请求,然后将其发送到User Forum用户邮件列表