与许多平台不同,Java缺少连接存根库。 如果您已经在这个世界上呆了很长时间,那么您可能应该熟悉WireMock,Betamax甚至Spock。 测试中的许多开发人员使用Mockito描述对象的行为,带有本地h2数据库的DataJpaTest和Cucumber测试。 今天,您将遇到一个轻量级的替代方案,它将帮助您解决使用这些方法可能遇到的各种问题。 特别地,anyStub尝试解决以下问题:
- 简化测试环境配置
- 自动收集测试数据
- 继续测试您的应用程序,避免测试其他内容
什么是anyStub及其运作方式
AnyStub包装函数调用,尝试查找已记录的匹配调用。 这样会发生两件事:
- 如果存在匹配的调用,则anyStub将恢复与该调用关联的记录结果并返回
- 如果没有匹配的调用,并且允许访问外部系统,则anyStub会进行此调用,记录该结果并返回
开箱即用,anyStub提供来自Apache HttpClient的http客户端的包装器,用于为http请求和来自javax.sql的多个接口创建存根*用于数据库连接。 还为您提供了一个API,用于为其他连接创建存根。
AnyStub是一个简单的类库,不需要对环境进行特殊配置。 该库旨在使用Spring Boot应用程序,通过遵循以下路径,您将获得最大的收益。 您可以在Spring之外的普通Java应用程序中使用它,但是肯定需要做更多的工作。 以下描述集中于测试Spring Boot应用程序。
让我们看一下集成测试。 这是测试系统的最令人兴奋和最全面的方法。 实际上,当您编写魔术注释时,spring-boot和JUnit几乎可以为您做所有事情:
@RunWith(SpringRunner.class) @SpringBootTest
目前,集成测试被低估了并且在有限的程度上被使用,并且一些开发者避免了它们。 这主要是由于耗时的测试准备和维护,或者需要在构建服务器上对环境进行特殊配置。
使用anyStub,您不必削弱spring-contex。 相反,将上下文保持在生产配置附近非常简单明了。
在此示例中,我们将从Pivotal的手册中了解如何将anyStub连接到使用RESTful Web服务 。
通过pom.xml连接库
<dependency> <groupId>org.anystub</groupId> <artifactId>anystub</artifactId> <version>0.2.27</version> <scope>test</scope> </dependency>
下一步是修改spring上下文。
package hello; import org.anystub.http.StubHttpClient; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class TestConfiguration { @Bean public RestTemplateBuilder builder() { RestTemplateCustomizer restTemplateCustomizer = new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { HttpClient real = HttpClientBuilder.create().build(); StubHttpClient stubHttpClient = new StubHttpClient(real); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(stubHttpClient); restTemplate.setRequestFactory(requestFactory); } }; return new RestTemplateBuilder(restTemplateCustomizer); } }
此修改不会更改应用程序中的组件关系,而只会替换单个接口的实现。 这将我们带到了芭芭拉·里斯克(Barbara Lisk)替代原则 。 如果您的应用程序的设计没有违反它,则此替换不会违反功能。
一切准备就绪。 该项目已包含测试。
@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Autowired private RestTemplate restTemplate; @Test public void contextLoads() { assertThat(restTemplate).isNotNull(); } }
该测试为空,但是它已经在运行应用程序上下文。 乐趣从这里开始 。 如上所述,测试中的应用程序上下文与在其中创建CommandLineRunner的工作上下文一致,并在其中执行对外部系统的http请求。
@SpringBootApplication public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public static void main(String args[]) { SpringApplication.run(Application.class); } @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } @Bean public CommandLineRunner run(RestTemplate restTemplate) throws Exception { return args -> { Quote quote = restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); log.info(quote.toString()); }; } }
这足以演示该库的操作。 首次启动测试后,您将找到新的complete/src/test/resources/anystub/stub.yml
。
request0: exception: [] keys: [GET, HTTP/1.1, 'https://gturnquist-quoters.cfapps.io/api/random'] values: [HTTP/1.1, '200', OK, 'Content-Type: application/json;charset=UTF-8', 'Date: Thu, 25 Apr 2019 23:04:49 GMT', 'X-Vcap-Request-Id: 5ffce9f3-d972-4e95-6b5c-f88f9b0ae29b', 'Content-Length: 177', 'Connection: keep-alive', '{"type":"success","value":{"id":3,"quote":"Spring has come quite a ways in addressing developer enjoyment and ease of use since the last time I built an application using it."}}']
发生什么事了 spring-boot已将测试配置中的RestTemplateBuilder构建到应用程序中。 这导致应用程序通过http客户端的存根实现工作。 StubHttpClient拦截了请求,未找到存根文件,执行了请求,将结果保存在文件中,并返回了从文件中恢复的结果。
从现在开始,您可以在没有Internet连接的情况下运行此测试,并且此请求将成功。 restTemplate.getForObject()
将返回相同的结果。 您可以在以后的测试中依靠这一事实。
您可以在GitHub上找到所有描述的更改。
实际上,我们仍未创建单个测试。 在编写测试之前,让我们看看它如何与数据库一起工作。
在此示例中,我们将向Pivotal教程中的使用带有Spring的JDBC访问关系数据添加集成测试。
这种情况下的测试配置如下所示:
package hello; import org.anystub.jdbc.StubDataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class TestConfiguration { @Bean public DataSource dataSource() { JdbcDataSource ds = new JdbcDataSource(); ds.setURL("jdbc:h2:./test"); return new StubDataSource(ds); } }
在这里,将创建到外部数据库的常规数据源,并使用存根实现(StubDataSource类)进行包装。 Spring-boot将其嵌入上下文中。 我们还需要创建至少一个测试以在测试中运行spring上下文。
package hello; import org.anystub.AnyStubId; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Test @AnyStubId public void test() { } }
这又是一个空测试-它的唯一任务是运行应用程序上下文。 在这里,我们看到了一个非常重要的注释@AnystubId
,但尚未涉及到它。
第一次运行后,您将找到一个包含所有数据库调用的新src/test/resources/anystub/stub.yml
。 您会惊讶于spring在数据库中如何在后台运行。 请注意,测试的新运行不会导致对数据库的真正访问。 如果删除test.mv.db,则在重复运行测试后它不会出现。 完整的更改集可以在GitHub上查看。
总结一下。 与anyStub:
- 您不需要专门设置测试环境
- 使用真实数据进行测试
- 测试的第一次运行证明了您的假设并保存了测试数据,随后的测试检查了系统是否未降级
您可能有疑问:这将如何涵盖数据库尚不存在的情况,如何进行负面测试和异常处理。 我们将回到这一点,但是首先,我们将处理编写简单的测试。
现在,我们正在尝试使用RESTful Web服务 。 该项目不包含可以测试的组件。 下面创建了两个类,它们应该描述某些体系结构设计的两层。
package hello; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class DataProvider { private final RestTemplate restTemplate; public DataProvider(RestTemplate restTemplate) { this.restTemplate = restTemplate; } Quote provideData() { return restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); } }
DataProvider提供对以下数据的访问 易挥发的 外部系统。
package hello; import org.springframework.stereotype.Component; @Component public class DataProcessor { private final DataProvider dataProvider; public DataProcessor(DataProvider dataProvider) { this.dataProvider = dataProvider; } int processData() { return dataProvider.provideData().getValue().getQuote().length(); } }
DataProcessor将处理来自外部系统的数据。
我们打算测试DataProcessor
。 有必要测试处理算法的正确性,并保护系统免受将来更改的影响。
为了实现这些目标,您可以考虑使用数据集创建DataProvider模拟对象,并将其传递给测试中的DataProcessor构造函数。 另一种方法是分解DataProcessor以突出显示Quote类的处理。 然后,使用单元测试很容易测试此类(当然,这是有关纯净代码的书籍中推荐的方法)。 让我们尝试避免代码更改和测试数据的发明,只编写一个测试。
@RunWith(SpringRunner.class) @SpringBootTest public class DataProcessorTest { @Autowired private DataProcessor dataProcessor; @Test @AnyStubId(filename = "stub") public void processDataTest() { assertEquals(131, dataProcessor.processData()); } }
是时候谈论@AnystubId注释了。 此批注有助于管理和控制测试中的存根文件。 它可以与测试类或其方法一起使用。 该注释为相应区域设置了一个单独的存根文件。 如果类和方法级别的注释同时覆盖了任何区域,则方法注释优先。 此批注具有filename参数,该参数定义存根文件的名称。 如果省略扩展名“ .yml”,则会自动添加。 通过运行此测试,您将找不到新文件。 src/test/resources/anystub/stub.yml
已在前面创建,该测试将重新使用它。 通过分析查询结果,我们从该存根中获得了数字131。
@Test @AnyStubId public void processDataTest2() { assertEquals(131, dataProcessor.processData()); Base base = getStub(); assertEquals(1, base.times("GET")); assertTrue(base.history().findFirst().get().matchEx_to(null, null, ".*gturnquist-quoters.cfapps.io.*")); }
在此测试中,@ AnyStubId批注不带文件名参数出现。 在这种情况下,将使用src/test/resources/anystubprocessDataTest2.yml
。 文件名由函数(类)的名称+“ .yml”构建。 一旦anyStub为此测试创建了新文件,您就需要进行实际的系统调用。 新报价具有相同的长度是我们的幸运。 最后两项检查显示了如何测试应用程序的行为。 您可以使用它:按参数或部分参数选择查询并计算查询数量。 在文档中可以找到时间和匹配功能的几种变化。
@Test @AnyStubId(requestMode = RequestMode.rmTrack) public void processDataTest3() { assertEquals(79, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); assertEquals(168, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); Base base = getStub(); assertEquals(4, base.times("GET")); }
在此测试中,@ AnyStubId与新的requestMode参数一起出现。 它允许您管理存根文件的权限。 有两个方面需要控制:文件搜索和调用外部系统的权限。
RequestMode.rmTrack
设置以下规则:如果刚刚创建了文件,则无论文件中是否存在相同的请求,所有请求都将发送到外部系统并带有答案写入文件(允许重复文件)。 如果在运行测试之前存根文件存在,则禁止对外部系统的请求。 呼叫的顺序应该完全相同。 如果下一个请求与文件中的请求不匹配,则会引发异常。
RequestMode.rmNew
此模式默认情况下RequestMode.rmNew
激活状态。 在存根文件中搜索每个请求。 如果找到匹配的请求-从文件中恢复相应的结果,则对外部系统的请求将被推迟。 如果找不到请求,则请求外部系统,结果保存在文件中。 文件中的重复请求-不会发生。
RequestMode.rmNone
在存根文件中查找每个请求。 如果找到匹配的查询,其结果将从文件中恢复。 如果测试生成的请求不在文件中,则会引发异常。
RequestMode.rmAll
在第一个请求之前,将清除存根文件。 所有请求均写入文件(允许文件中重复)。 如果要观看连接工作,可以使用此模式。
RequestMode.rmPassThrough
所有请求直接发送到外部系统,而绕过了实现存根。
这些更改在GitHub上可用。
还有什么
我们看到了anyStub如何保存响应。 如果在访问外部系统时引发异常,则anyStub将保存该异常,并在后续请求中播放该异常。
顶级类通常会引发异常,而连接类会收到有效的响应(可能带有错误代码)。 在这种情况下,anyStub负责再现带有错误代码的答案,并且更高级别的类还将为测试抛出异常。
将存根文件添加到存储库。
不要害怕删除和覆盖存根文件。
明智地管理存根文件。 您可以在多个测试中重用一个文件,也可以为每个测试提供一个文件。 利用这个机会来满足您的需求。 但是通常使用具有不同访问模式的单个文件是一个坏主意。
这些是anyStub的所有主要功能。