Ao contrário de muitas plataformas, o Java sofre com a falta de bibliotecas de stub de conexão. Se você está neste mundo há muito tempo, provavelmente deve estar familiarizado com o WireMock, Betamax ou mesmo Spock. Muitos desenvolvedores nos testes usam o Mockito para descrever o comportamento dos objetos, DataJpaTest com um banco de dados h2 local, testes de pepino. Hoje, você encontrará uma alternativa leve que o ajudará a lidar com vários problemas que você pode encontrar usando essas abordagens. Em particular, o anyStub tenta resolver os seguintes problemas:
- simplifique a configuração do ambiente de teste
- automatize a coleta de dados para testes
- fique testando sua aplicação e evite testar outras coisas
O que é o anyStub e como ele funciona
O AnyStub agrupa chamadas de função, tentando encontrar chamadas correspondentes que já foram gravadas. Duas coisas podem acontecer com isso:
- se houver uma chamada correspondente, o anyStub restaurará o resultado registrado associado a essa chamada e o retornará
- se não houver chamada correspondente e o acesso ao sistema externo for permitido, o anyStub fará essa chamada, registrará esse resultado e retornará
Pronto, o anyStub fornece wrappers para o cliente http do Apache HttpClient para criar stubs para solicitações http e várias interfaces do javax.sql. * Para conexões com o banco de dados. Você também recebe uma API para criar stubs para outras conexões.
AnyStub é uma biblioteca de classes simples e não requer configuração especial do seu ambiente. Esta biblioteca tem como objetivo trabalhar com aplicativos de inicialização por mola e você obterá o benefício máximo seguindo este caminho. Você pode usá-lo fora do Spring, em aplicativos Java simples, mas definitivamente precisará fazer um trabalho adicional. A descrição a seguir é focada no teste de aplicativos de inicialização por mola.
Vamos dar uma olhada nos testes de integração. Esta é a maneira mais emocionante e abrangente de testar seu sistema. De fato, spring-boot e JUnit fazem quase tudo por você quando você escreve anotações mágicas:
@RunWith(SpringRunner.class) @SpringBootTest
No momento, os testes de integração são subestimados e usados em uma extensão limitada, e alguns desenvolvedores os evitam. Isso se deve principalmente à demorada preparação e manutenção de testes ou à necessidade de configuração especial do ambiente nos servidores de construção.
Com o anyStub, você não precisa aleijar o contexto da primavera. Em vez disso, manter o contexto próximo à configuração de produção é simples e direto.
Neste exemplo, veremos como conectar anyStub a um serviço da Web Consumindo um RESTful no manual do Pivotal.
Conectando uma biblioteca através do pom.xml
<dependency> <groupId>org.anystub</groupId> <artifactId>anystub</artifactId> <version>0.2.27</version> <scope>test</scope> </dependency>
O próximo passo é modificar o contexto da primavera.
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); } }
Essa modificação não altera os relacionamentos dos componentes no aplicativo, mas substitui apenas a implementação de uma única interface. Isso nos envia ao Princípio de Substituição de Barbara Lisk . Se o design do seu aplicativo não o violar, essa substituição não violará a funcionalidade.
Está tudo pronto. Este projeto já inclui um teste.
@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Autowired private RestTemplate restTemplate; @Test public void contextLoads() { assertThat(restTemplate).isNotNull(); } }
Este teste está vazio, mas já está executando o contexto do aplicativo. A diversão começa aqui . Como dissemos acima, o contexto do aplicativo no teste coincide com o contexto de trabalho no qual o CommandLineRunner é criado no qual a solicitação http para o sistema externo é executada.
@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()); }; } }
Isso é suficiente para demonstrar o funcionamento da biblioteca. Após iniciar os testes pela primeira vez, você encontrará o novo 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."}}']
O que aconteceu O spring-boot criou RestTemplateBuilder a partir da configuração de teste no aplicativo. Isso levou o aplicativo a trabalhar com a implementação do stub do cliente http. StubHttpClient interceptou a solicitação, não encontrou o arquivo stub, executou a solicitação, salvou o resultado em um arquivo e retornou o resultado recuperado do arquivo.
A partir de agora, você pode executar este teste sem uma conexão à Internet e essa solicitação será bem-sucedida. restTemplate.getForObject()
retornará o mesmo resultado. Você pode confiar nesse fato em seus testes futuros.
Você pode encontrar todas as alterações descritas no GitHub .
De fato, ainda não criamos um único teste. Antes de escrever testes, vamos ver como ele funciona com bancos de dados.
Neste exemplo, adicionaremos um teste de integração ao Accessing Relational Data usando JDBC with Spring no tutorial Pivotal.
A configuração de teste para este caso é semelhante a esta:
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); } }
Aqui, uma fonte de dados regular para um banco de dados externo é criada e agrupada com uma implementação de stub - a classe StubDataSource. O Spring-boot o incorpora no contexto. Também precisamos criar pelo menos um teste para executar o contexto de primavera no teste.
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() { } }
Este é novamente um teste vazio - sua única tarefa é executar o contexto do aplicativo. Aqui vemos uma anotação muito importante @AnystubId
, mas ela não estará envolvida ainda.
Após a primeira execução, você encontrará um novo src/test/resources/anystub/stub.yml
que inclui todas as chamadas ao banco de dados. Você ficará surpreso como a primavera funciona nos bastidores com bancos de dados. Observe que, novas execuções do teste não levarão a um acesso real ao banco de dados. Se você excluir test.mv.db, ele não aparecerá após execuções repetidas dos testes. O conjunto completo de alterações pode ser visualizado no GitHub .
Para resumir. com anyStub:
- você não precisa configurar especificamente um ambiente de teste
- o teste é realizado com dados reais
- a primeira execução dos testes comprova suas suposições e salva os dados de teste; as subseqüentes verificam se o sistema não foi degradado
Você provavelmente tem perguntas: como isso cobre casos em que o banco de dados ainda não existe, o que fazer com testes negativos e tratamento de exceções. Voltaremos a isso, mas primeiro, trataremos de escrever testes simples.
Agora estamos experimentando Consumir um serviço da Web RESTful . Este projeto não contém componentes que podem ser testados. Duas classes são criadas abaixo, que devem representar duas camadas de algum design de arquitetura.
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); } }
O DataProvider fornece acesso aos dados em volátil sistema externo.
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(); } }
O DataProcessor processará dados de um sistema externo.
Pretendemos testar o DataProcessor
. É necessário testar a correção do algoritmo de processamento e proteger o sistema da degradação de alterações futuras.
Para atingir esses objetivos, considere criar um objeto simulado DataProvider com um conjunto de dados e passá-lo ao construtor DataProcessor nos testes. Outra maneira poderia ser decompor o DataProcessor para destacar o processamento da classe Quote. Em seguida, é fácil testar essa classe usando testes de unidade (certamente, este é o método recomendado em livros respeitados sobre código limpo). Vamos tentar evitar alterações de código e a invenção dos dados de teste e apenas escrever um teste.
@RunWith(SpringRunner.class) @SpringBootTest public class DataProcessorTest { @Autowired private DataProcessor dataProcessor; @Test @AnyStubId(filename = "stub") public void processDataTest() { assertEquals(131, dataProcessor.processData()); } }
Hora de falar sobre a anotação @AnystubId. Essa anotação ajuda a gerenciar e controlar arquivos stub em testes. Pode ser usado com uma classe de teste ou seu método. Esta anotação configura um arquivo stub individual para a área correspondente. Se qualquer área for coberta simultaneamente por anotações de nível de classe e método, a anotação de método terá precedência. Esta anotação possui o parâmetro filename, que define o nome do arquivo stub. a extensão ".yml" é adicionada automaticamente se omitida. Ao executar este teste, você não encontrará um novo arquivo. O src/test/resources/anystub/stub.yml
já foi criado anteriormente e este teste o reutilizará. Obtivemos o número 131 desse esboço analisando o resultado da consulta.
@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.*")); }
Neste teste, a anotação @AnyStubId aparece sem o parâmetro filename. Nesse caso, o src/test/resources/anystubprocessDataTest2.yml
. O nome do arquivo é criado a partir do nome da função (classe) + ".yml". Depois que o anyStub criar um novo arquivo para este teste, você precisará fazer uma chamada de sistema real. E é nossa sorte que a nova cotação tenha a mesma duração. As duas últimas verificações mostram como testar o comportamento do aplicativo. Está disponível para você: selecionando consultas por parâmetros ou partes de parâmetros e contando o número de consultas. Existem várias variações de horários e funções de correspondência que podem ser encontradas na documentação .
@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")); }
Neste teste, @AnyStubId aparece com o novo parâmetro requestMode. Ele permite que você gerencie permissões para arquivos stub. Há dois aspectos a serem controlados: pesquisa de arquivos e permissão para chamar um sistema externo.
RequestMode.rmTrack
define as seguintes regras: se o arquivo acabou de ser criado, todas as solicitações são enviadas ao sistema externo e gravadas no arquivo com as respostas, independentemente de haver uma solicitação idêntica no arquivo (são permitidas duplicatas no arquivo). Se antes da execução dos testes o arquivo stub existir, as solicitações ao sistema externo serão proibidas. As chamadas são esperadas exatamente na mesma sequência. Se a próxima solicitação não corresponder à solicitação no arquivo, uma exceção será lançada.
RequestMode.rmNew
modo é ativado por padrão. Cada solicitação é pesquisada no arquivo stub. Se uma solicitação correspondente for encontrada - o resultado correspondente é restaurado do arquivo, a solicitação para o sistema externo é adiada. Se a solicitação não for encontrada, o sistema externo será solicitado, o resultado será salvo em um arquivo. Solicitações duplicadas no arquivo - não ocorrem.
RequestMode.rmNone
Cada solicitação é pesquisada em um arquivo stub. Se uma consulta correspondente for encontrada, seu resultado será restaurado do arquivo. Se o teste gerar uma solicitação que não esteja no arquivo, uma exceção será lançada.
RequestMode.rmAll
antes da primeira solicitação, o arquivo stub é limpo. Todas as solicitações são gravadas no arquivo (duplicatas no arquivo são permitidas). Você pode usar este modo se quiser assistir a conexão funcionar.
RequestMode.rmPassThrough
todas as solicitações são enviadas diretamente ao sistema externo, ignorando o stub de implementação.
Essas alterações estão disponíveis no GitHub.
O que mais?
Vimos como o anyStub salva respostas. Se uma exceção for lançada ao acessar um sistema externo, o anyStub salvará e reproduzirá em solicitações subsequentes.
Geralmente, as exceções são geradas pelas classes de nível superior, enquanto as classes de conexão recebem uma resposta válida (provavelmente com um código de erro). Nesse caso, o anyStub é responsável por reproduzir a própria resposta com o código de erro, e as classes de nível superior também lançarão exceções para seus testes.
Inclua arquivos stub no repositório.
Não tenha medo de excluir e substituir arquivos stub.
Gerencie arquivos stub com sabedoria. Você pode reutilizar um arquivo em vários testes ou fornecer um arquivo individual para cada teste. Use esta oportunidade para suas necessidades. Mas geralmente usar um único arquivo com diferentes modos de acesso é uma má ideia.
Esses são os principais recursos do anyStub.