Para ter cobertura de código suficiente e para criar novas funcionalidades e refatorar as antigas sem medo de quebrar algo, os testes devem ser fáceis de ler e de manter. Neste artigo, falarei sobre muitas técnicas para escrever testes de unidade e integração em Java que coletei ao longo dos anos. Vou contar com tecnologias modernas: JUnit5, AssertJ, Testcontainers, e também não vou ignorar Kotlin. Algumas das dicas parecerão óbvias para você, outras podem ir contra o que você leu em livros sobre desenvolvimento e teste de software.
Em poucas palavras
- Escreva testes de forma concisa e específica, usando funções auxiliares, parametrização, várias primitivas da biblioteca AssertJ, não abuse das variáveis, verifique apenas o que está relacionado à funcionalidade testada e não cole todos os casos não padrão em um teste
- , ,
- , -,
- KISS DRY
- , , , in-memory-
- JUnit5 AssertJ —
- : , , Clock - .
Given, When, Then (, , )
O teste deve conter três blocos, separados por linhas em branco. Cada bloco deve ser o mais curto possível. Use métodos locais para manter as coisas compactas.
Dado / Dado (entrada): preparação do teste, por exemplo, criação de dados e configuração de simulação.
Quando (ação): chame o método testado
Then / To (saída): verifique a exatidão do valor recebido
//
@Test
public void findProduct() {
insertIntoDatabase(new Product(100, "Smartphone"));
Product product = dao.findProduct(100);
assertThat(product.getName()).isEqualTo("Smartphone");
}
Use os prefixos “real *” e “esperado *”
//
ProductDTO product1 = requestProduct(1);
ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);
Se você for usar variáveis em um teste de correspondência, adicione os prefixos “real” e “esperado” a essas variáveis. Isso irá melhorar a legibilidade do seu código e esclarecer o propósito das variáveis. Também os torna mais difíceis de confundir durante a comparação.
//
ProductDTO actualProduct = requestProduct(1);
ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); //
Use valores predefinidos em vez de aleatórios
Evite alimentar valores aleatórios para a entrada de testes. Isso pode levar a testes intermitentes, o que é muito difícil de depurar. Além disso, se você vir um valor aleatório em uma mensagem de erro, não será possível rastreá-lo até onde o erro ocorreu.
//
Instant ts1 = Instant.now(); // 1557582788
Instant ts2 = ts1.plusSeconds(1); // 1557582789
int randomAmount = new Random().nextInt(500); // 232
UUID uuid = UUID.randomUUID(); // d5d1f61b-0a8b-42be-b05a-bd458bb563ad
Use diferentes valores predefinidos para tudo. Desta forma, você obterá resultados de teste perfeitamente reproduzíveis, além de encontrar rapidamente o local correto no código pela mensagem de erro.
//
Instant ts1 = Instant.ofEpochSecond(1550000001);
Instant ts2 = Instant.ofEpochSecond(1550000002);
int amount = 50;
UUID uuid = UUID.fromString("00000000-000-0000-0000-000000000001");
Você pode escrever isso ainda mais curto usando funções auxiliares (veja abaixo).
Escreva testes concisos e específicos
Use funções auxiliares sempre que possível
Isole o código repetitivo em funções locais e dê-lhes nomes significativos. Isso manterá seus testes compactos e fáceis de ler à primeira vista.
//
@Test
public void categoryQueryParameter() throws Exception {
List<ProductEntity> products = List.of(
new ProductEntity().setId("1").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1),
new ProductEntity().setId("2").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1),
new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2)
);
for (ProductEntity product : products) {
template.execute(createSqlInsertStatement(product));
}
String responseJson = client.perform(get("/products?category=Office"))
.andExpect(status().is(200))
.andReturn().getResponse().getContentAsString();
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("1", "2");
}
//
@Test
public void categoryQueryParameter2() throws Exception {
insertIntoDatabase(
createProductWithCategory("1", "Office"),
createProductWithCategory("2", "Office"),
createProductWithCategory("3", "Hardware")
);
String responseJson = requestProductsByCategory("Office");
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("1", "2");
}
- use funções auxiliares para criar dados (objetos) (
createProductWithCategory()) e verificações complexas. Passe apenas os parâmetros para as funções auxiliares que são relevantes neste teste; para o resto, use os padrões adequados. No Kotlin, existem valores de parâmetro padrão para isso, e em Java você pode usar cadeias de chamada de método e sobrecarga para simular os parâmetros padrão. - lista de parâmetros de comprimento variável tornará seu código ainda mais elegante (
ìnsertIntoDatabase()) - funções auxiliares também podem ser usadas para criar valores simples. Kotlin faz isso ainda melhor por meio de funções de extensão.
// (Java)
Instant ts = toInstant(1); // Instant.ofEpochSecond(1550000001)
UUID id = toUUID(1); // UUID.fromString("00000000-0000-0000-a000-000000000001")
// (Kotlin)
val ts = 1.toInstant()
val id = 1.toUUID()
As funções auxiliares no Kotlin podem ser implementadas assim:
fun Int.toInstant(): Instant = Instant.ofEpochSecond(this.toLong())
fun Int.toUUID(): UUID = UUID.fromString("00000000-0000-0000-a000-${this.toString().padStart(11, '0')}")
Não abuse das variáveis
O reflexo condicionado do programador é mover os valores usados com frequência para as variáveis.
//
@Test
public void variables() throws Exception {
String relevantCategory = "Office";
String id1 = "4243";
String id2 = "1123";
String id3 = "9213";
String irrelevantCategory = "Hardware";
insertIntoDatabase(
createProductWithCategory(id1, relevantCategory),
createProductWithCategory(id2, relevantCategory),
createProductWithCategory(id3, irrelevantCategory)
);
String responseJson = requestProductsByCategory(relevantCategory);
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly(id1, id2);
}
Infelizmente, isso é uma grande sobrecarga de código. Pior, ver o valor na mensagem de erro será impossível rastrear até onde o erro ocorreu.
"KISS é mais importante que DRY"
//
@Test
public void variables() throws Exception {
insertIntoDatabase(
createProductWithCategory("4243", "Office"),
createProductWithCategory("1123", "Office"),
createProductWithCategory("9213", "Hardware")
);
String responseJson = requestProductsByCategory("Office");
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("4243", "1123");
}
Se você está tentando escrever testes o mais compactos possível (o que eu recomendo vivamente de qualquer maneira), então os valores reutilizados são claramente visíveis. O próprio código se torna mais compacto e mais legível. Finalmente, a mensagem de erro o levará à linha exata onde o erro ocorreu.
Não estenda os testes existentes para "adicionar mais uma coisinha"
//
public class ProductControllerTest {
@Test
public void happyPath() {
// ...
}
}
É sempre tentador adicionar um caso especial a um teste existente que valida a funcionalidade básica. Mas, como resultado, os testes ficam maiores e mais difíceis de entender. Casos especiais espalhados por uma grande folha de código são fáceis de perder. Se o teste falhar, você pode não entender imediatamente o que exatamente o causou.
//
public class ProductControllerTest {
@Test
public void multipleProductsAreReturned() {}
@Test
public void allProductValuesAreReturned() {}
@Test
public void filterByCategory() {}
@Test
public void filterByDateCreated() {}
}
Em vez disso, escreva um novo teste com um nome descritivo que torne imediatamente claro qual comportamento ele espera do código em teste. Sim, você terá que digitar mais letras no teclado (contra isso, deixe-me lembrá-lo, as funções auxiliares ajudam muito), mas você obterá um teste simples e compreensível com um resultado previsível. Essa é uma ótima maneira de documentar novas funcionalidades, a propósito.
Marque apenas o que você deseja testar
Pense na funcionalidade que você está testando. Evite fazer verificações desnecessárias simplesmente porque você pode. Além disso, fique atento ao que já foi testado em testes escritos anteriormente e não volte a testar. Os testes devem ser compactos e seu comportamento esperado deve ser óbvio e desprovido de detalhes desnecessários.
Digamos que desejamos testar um identificador HTTP que retorna uma lista de produtos. Nosso conjunto de testes deve conter os seguintes testes:
1. Um grande teste de mapeamento que verifica se todos os valores do banco de dados são retornados corretamente na resposta JSON e são atribuídos corretamente no formato correto. Podemos escrever isso facilmente usando as funções
isEqualTo()(para um único item) ou containsOnly()(para vários itens) do pacote AssertJ, se você implementar o método corretamenteequals()...
String responseJson = requestProducts();
ProductDTO expectedDTO1 = new ProductDTO("1", "envelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED));
ProductDTO expectedDTO2 = new ProductDTO("2", "envelope", new Category("smartphone"), List.of(States.ACTIVE));
assertThat(toDTOs(responseJson))
.containsOnly(expectedDTO1, expectedDTO2);
2. Vários testes que verificam o comportamento correto do parâmetro? Categoria. Aqui queremos apenas verificar se os filtros estão funcionando corretamente, não os valores das propriedades, porque já fizemos isso antes. Portanto, é suficiente para nós verificarmos as correspondências do id do produto recebido:
String responseJson = requestProductsByCategory("Office");
assertThat(toDTOs(responseJson))
.extracting(ProductDTO::getId)
.containsOnly("1", "2");
3. Mais alguns testes que verificam casos especiais ou lógica de negócios especial, por exemplo, se certos valores na resposta são calculados corretamente. Nesse caso, estamos interessados apenas em alguns campos de toda a resposta JSON. Portanto, estamos documentando essa lógica especial com nosso teste. É claro que não precisamos de nada além desses campos aqui.
assertThat(actualProduct.getPrice()).isEqualTo(100);
Testes independentes
Não oculte parâmetros relevantes (em funções auxiliares)
//
insertIntoDatabase(createProduct());
List<ProductDTO> actualProducts = requestProductsByCategory();
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));
É conveniente usar funções auxiliares para gerar dados e verificar as condições, mas elas devem ser chamadas com parâmetros. Aceite parâmetros para tudo o que é significativo no teste e deve ser controlado a partir do código de teste. Não force o leitor a pular para a função auxiliar para entender o significado do teste. Uma regra simples: o significado do teste deve ser claro quando se olha para o teste em si.
//
insertIntoDatabase(createProduct("1", "Office"));
List<ProductDTO> actualProducts = requestProductsByCategory("Office");
assertThat(actualProducts).containsOnly(new ProductDTO("1", "Office"));
Mantenha os dados de teste dentro dos próprios testes
Tudo deve estar dentro. É tentador mover alguns dos dados para um método
@Beforee reutilizá-los a partir daí. Mas isso forçará o leitor a ir e vir no arquivo para entender o que exatamente está acontecendo aqui. Novamente, as funções auxiliares o ajudarão a evitar a repetição e tornar seus testes mais fáceis de entender.
Use composição em vez de herança
Não crie hierarquias de classe de teste complexas.
//
class SimpleBaseTest {}
class AdvancedBaseTest extends SimpleBaseTest {}
class AllInklusiveBaseTest extends AdvancedBaseTest {}
class MyTest extends AllInklusiveBaseTest {}
Essas hierarquias complicam o entendimento e você, muito provavelmente, rapidamente se verá escrevendo o próximo sucessor do teste básico, dentro do qual é costurado um monte de lixo de que o teste atual não precisa de nada. Isso distrai o leitor e leva a erros sutis. A herança não é flexível: você acha que pode usar todos os métodos de uma classe
AllInclusiveBaseTest, mas nenhum de seu pai ? AdvancedBaseTest?Além disso, o leitor terá que pular constantemente entre diferentes classes base para entender o quadro geral.
“É melhor duplicar o código do que escolher a abstração errada” (Sandi Metz)
Eu recomendo usar composição em vez disso. Escreva pequenos fragmentos e classes para cada tarefa relacionada ao aparelho (iniciar um banco de dados de teste, criar um esquema, inserir dados, iniciar um servidor simulado). Reutilize essas partes em um método
@BeforeAllou atribuindo os objetos criados aos campos da classe de teste. Dessa forma, você poderá construir cada nova classe de teste a partir desses espaços, a partir de peças de Lego. Como resultado, cada teste terá seu próprio conjunto compreensível de acessórios e garantirá que nada fora dele aconteça. O teste se torna autossuficiente porque contém tudo o que você precisa.
//
public class MyTest {
//
private JdbcTemplate template;
private MockWebServer taxService;
@BeforeAll
public void setupDatabaseSchemaAndMockWebServer() throws IOException {
this.template = new DatabaseFixture().startDatabaseAndCreateSchema();
this.taxService = new MockWebServer();
taxService.start();
}
}
//
public class DatabaseFixture {
public JdbcTemplate startDatabaseAndCreateSchema() throws IOException {
PostgreSQLContainer db = new PostgreSQLContainer("postgres:11.2-alpine");
db.start();
DataSource dataSource = DataSourceBuilder.create()
.driverClassName("org.postgresql.Driver")
.username(db.getUsername())
.password(db.getPassword())
.url(db.getJdbcUrl())
.build();
JdbcTemplate template = new JdbcTemplate(dataSource);
SchemaCreator.createSchema(template);
return template;
}
}
De novo:
"KISS é mais importante que DRY"
Testes diretos são bons. Compare o resultado com constantes
Não reutilize o código de produção
Os testes devem validar o código de produção, não reutilizá-lo. Se você reutilizar o código de combate em um teste, poderá perder um bug nesse código porque não o está mais testando.
//
boolean isActive = true;
boolean isRejected = true;
insertIntoDatabase(new Product(1, isActive, isRejected));
ProductDTO actualDTO = requestProduct(1);
//
List<State> expectedStates = ProductionCode.mapBooleansToEnumList(isActive, isRejected);
assertThat(actualDTO.states).isEqualTo(expectedStates);
Em vez disso, pense em termos de entrada e saída ao escrever testes. O teste alimenta dados para a entrada e compara a saída com constantes predefinidas. Na maioria das vezes, a reutilização de código não é necessária.
// Do
assertThat(actualDTO.states).isEqualTo(List.of(States.ACTIVE, States.REJECTED));
Não copie a lógica de negócios para os testes
O mapeamento de objetos é um excelente exemplo de caso em que os testes puxam a lógica do código de combate para si mesmos. Suponha que nosso teste contenha um método
mapEntityToDto(), o resultado do qual é usado para verificar se o DTO resultante contém os mesmos valores que os elementos que foram adicionados à base no início do teste. Nesse caso, você provavelmente copiará o código de combate para o teste, que pode conter erros.
//
ProductEntity inputEntity = new ProductEntity(1, "envelope", "office", false, true, 200, 10.0);
insertIntoDatabase(input);
ProductDTO actualDTO = requestProduct(1);
// mapEntityToDto() , -
ProductDTO expectedDTO = mapEntityToDto(inputEntity);
assertThat(actualDTO).isEqualTo(expectedDTO);
A solução correta é
actualDTOcompará-lo a um objeto de referência criado manualmente com os valores especificados. É extremamente simples, direto e protege contra possíveis erros.
//
ProductDTO expectedDTO = new ProductDTO("1", "envelope", new Category("office"), List.of(States.ACTIVE, States.REJECTED))
assertThat(actualDTO).isEqualTo(expectedDTO);
Se você não deseja criar e verificar se há uma correspondência para um objeto de referência inteiro, você pode verificar o objeto filho ou geralmente apenas as propriedades do objeto que são relevantes para o teste.
Não escreva muita lógica
Deixe-me lembrá-lo de que o teste é principalmente sobre entrada e saída. Envie os dados e verifique o que é devolvido a você. Não há necessidade de escrever lógica complexa dentro dos testes. Se você introduzir loops e condições em um teste, você o tornará menos compreensível e mais sujeito a erros. Se sua lógica de validação for complexa, use as várias funções AssertJ para fazer o trabalho para você.
Execute testes em um ambiente de combate
Teste o pacote de componentes mais completo possível
Geralmente, é recomendado testar cada classe isoladamente, usando simulações. Essa abordagem, entretanto, tem desvantagens: dessa forma, a interação das classes entre si não é testada, e qualquer refatoração de entidades gerais quebrará todos os testes de uma vez, porque cada classe interna tem seus próprios testes. Além disso, se você escrever testes para cada classe, simplesmente haverá muitos deles.
Teste de unidade isolada de cada classe
Em vez disso, recomendo focar em testes de integração. Por "teste de integração", quero dizer coletar todas as classes juntas (como na produção) e testar todo o pacote, incluindo os componentes de infraestrutura (servidor HTTP, banco de dados, lógica de negócios). Nesse caso, você está testando o comportamento em vez da implementação. Esses testes são mais precisos, mais próximos do mundo real e resistentes à refatoração de componentes internos. Idealmente, uma classe de testes será suficiente.
Teste de integração (= colocar todas as classes juntas e testar o pacote)
Não use bancos de dados na memória para testes
Com uma base in-memory, você testa em um ambiente diferente onde seu código funcionará.
Usando uma base in-memory ( H2 , HSQLDB , Fongo ) para testes, você sacrifica sua validade e escopo. Esses bancos de dados geralmente se comportam de maneira diferente e produzem resultados diferentes. Esse teste pode passar com sucesso, mas não garante o funcionamento correto do aplicativo em produção. Além disso, você pode facilmente se encontrar em uma situação em que não pode usar ou testar algum comportamento ou característica de recurso de sua base, porque eles não estão implementados no banco de dados na memória ou se comportam de maneira diferente.
Solução: use o mesmo banco de dados da operação real. Biblioteca maravilhosa de contêineres de teste fornece uma API rica para aplicativos Java que permite gerenciar contêineres diretamente de seu código de teste.
Java / JVM
Usar -noverify -XX:TieredStopAtLevel=1
Sempre adicione opções
JVM -noverify -XX:TieredStopAtLevel=1à sua configuração para executar testes. Isso economizará 1 a 2 segundos ao iniciar a máquina virtual antes de executar os testes. Isso é especialmente útil nos primeiros dias de seus testes, quando você frequentemente os executa a partir do IDE.
Observe que, como o Java 13 está
-noverifyobsoleto.
Dica: Adicione esses argumentos ao modelo de configuração “JUnit” no IntelliJ IDEA para evitar ter que fazer isso sempre que criar um novo projeto.
Use AssertJ
AssertJ é uma biblioteca extremamente poderosa e madura com uma API rica e segura, bem como um rico conjunto de funções de validação de valor e mensagens de erro de teste informativas. Muitas funções de validação convenientes dispensam o programador da necessidade de descrever a lógica complexa no corpo dos testes, permitindo que eles façam os testes concisos. Por exemplo:
assertThat(actualProduct)
.isEqualToIgnoringGivenFields(expectedProduct, "id");
assertThat(actualProductList).containsExactly(
createProductDTO("1", "Smartphone", 250.00),
createProductDTO("1", "Smartphone", 250.00)
);
assertThat(actualProductList)
.usingElementComparatorIgnoringFields("id")
.containsExactly(expectedProduct1, expectedProduct2);
assertThat(actualProductList)
.extracting(Product::getId)
.containsExactly("1", "2");
assertThat(actualProductList)
.anySatisfy(product -> assertThat(product.getDateCreated()).isBetween(instant1, instant2));
assertThat(actualProductList)
.filteredOn(product -> product.getCategory().equals("Smartphone"))
.allSatisfy(product -> assertThat(product.isLiked()).isTrue());
Evite usar assertTrue()eassertFalse()
Usando mensagens de erro de teste simples
assertTrue()ou assertFalse()criptografadas:
//
assertTrue(actualProductList.contains(expectedProduct));
assertTrue(actualProductList.size() == 5);
assertTrue(actualProduct instanceof Product);
expected: <true> but was: <false>
Em vez disso, use chamadas AssertJ, que retornam mensagens claras e informativas prontas para uso.
//
assertThat(actualProductList).contains(expectedProduct);
assertThat(actualProductList).hasSize(5);
assertThat(actualProduct).isInstanceOf(Product.class);
Expecting:
<[Product[id=1, name='Samsung Galaxy']]>
to contain:
<[Product[id=2, name='iPhone']]>
but could not find:
<[Product[id=2, name='iPhone']]>
Se você precisar verificar o valor booleano, torne a mensagem mais
as()descritiva com o método AssertJ.
Use JUnit5
JUnit5 é uma excelente biblioteca para teste (de unidade). Está em constante desenvolvimento e fornece ao programador muitos recursos úteis, como testes parametrizados, agrupamentos, testes condicionais, controle de ciclo de vida.
Use testes parametrizados
Os testes parametrizados permitem que você execute o mesmo teste com um conjunto de valores de entrada diferentes. Isso permite que você verifique vários casos sem escrever código extra. Em JUnit5 para isso é as excelentes ferramentas
@ValueSource, @EnumSource, @CsvSourcee @MethodSource.
//
@ParameterizedTest
@ValueSource(strings = ["§ed2d", "sdf_", "123123", "§_sdf__dfww!"])
public void rejectedInvalidTokens(String invalidToken) {
client.perform(get("/products").param("token", invalidToken))
.andExpect(status().is(400))
}
@ParameterizedTest
@EnumSource(WorkflowState::class, mode = EnumSource.Mode.INCLUDE, names = ["FAILED", "SUCCEEDED"])
public void dontProcessWorkflowInCaseOfAFinalState(WorkflowState itemsInitialState) {
// ...
}
Recomendo enfaticamente que você aproveite ao máximo esse truque, pois ele permite que você teste mais casos com o mínimo de esforço.
Por fim, gostaria de chamar sua atenção para
@CsvSourcee @MethodSource, que pode ser usado para parametrizações mais complexas, onde você também precisa controlar o resultado: você pode passá-lo em um dos parâmetros.
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"5, 3, 8",
"10, -20, -10"
})
public void add(int summand1, int summand2, int expectedSum) {
assertThat(calculator.add(summand1, summand2)).isEqualTo(expectedSum);
}
@MethodSourceespecialmente eficaz em conjunto com um objeto de teste separado contendo todos os parâmetros desejados e os resultados esperados. Infelizmente, em Java, a descrição de tais estruturas de dados (os chamados POJOs) é muito complicada. Portanto, vou dar um exemplo usando classes de dados Kotlin.
data class TestData(
val input: String?,
val expected: Token?
)
@ParameterizedTest
@MethodSource("validTokenProvider")
fun `parse valid tokens`(data: TestData) {
assertThat(parse(data.input)).isEqualTo(data.expected)
}
private fun validTokenProvider() = Stream.of(
TestData(input = "1511443755_2", expected = Token(1511443755, "2")),
TestData(input = "151175_13521", expected = Token(151175, "13521")),
TestData(input = "151144375_id", expected = Token(151144375, "id")),
TestData(input = "15114437599_1", expected = Token(15114437599, "1")),
TestData(input = null, expected = null)
)
Testes de grupo
A anotação
@Nesteddo JUnit5 é útil para agrupar métodos de teste. Logicamente, faz sentido agrupar certos tipos de testes (como InputIsXY, ErrorCases) ou reunir em seu grupo cada método de teste ( GetDesigne UpdateDesign).
public class DesignControllerTest {
@Nested
class GetDesigns {
@Test
void allFieldsAreIncluded() {}
@Test
void limitParameter() {}
@Test
void filterParameter() {}
}
@Nested
class DeleteDesign {
@Test
void designIsRemovedFromDb() {}
@Test
void return404OnInvalidIdParameter() {}
@Test
void return401IfNotAuthorized() {}
}
}
Nomes de teste legíveis com @DisplayNameou aspas em Kotlin
Em Java, você pode usar anotações
@DisplayNamepara dar nomes mais legíveis aos seus testes.
public class DisplayNameTest {
@Test
@DisplayName("Design is removed from database")
void designIsRemoved() {}
@Test
@DisplayName("Return 404 in case of an invalid parameter")
void return404() {}
@Test
@DisplayName("Return 401 if the request is not authorized")
void return401() {}
}
No Kotlin, você pode usar nomes de função com espaços dentro deles, colocando-os entre aspas simples crase. Dessa forma, você obtém legibilidade dos resultados sem redundância de código.
@Test
fun `design is removed from db`() {}
Simular serviços externos
Para testar clientes HTTP, precisamos simular os serviços que eles acessam. Costumo usar o MockWebServer da OkHttp para essa finalidade . As alternativas são WireMock ou Mockserver de Testcontainers .
MockWebServer serviceMock = new MockWebServer();
serviceMock.start();
HttpUrl baseUrl = serviceMock.url("/v1/");
ProductClient client = new ProductClient(baseUrl.host(), baseUrl.port());
serviceMock.enqueue(new MockResponse()
.addHeader("Content-Type", "application/json")
.setBody("{\"name\": \"Smartphone\"}"));
ProductDTO productDTO = client.retrieveProduct("1");
assertThat(productDTO.getName()).isEqualTo("Smartphone");
Use a capacidade de espera para testar o código assíncrono
Awaitility é uma biblioteca para testar código assíncrono. Você pode especificar quantas vezes repetir a verificação do resultado antes de declarar um teste malsucedido.
private static final ConditionFactory WAIT = await()
.atMost(Duration.ofSeconds(6))
.pollInterval(Duration.ofSeconds(1))
.pollDelay(Duration.ofSeconds(1));
@Test
public void waitAndPoll(){
triggerAsyncEvent();
WAIT.untilAsserted(() -> {
assertThat(findInDatabase(1).getState()).isEqualTo(State.SUCCESS);
});
}
Não há necessidade de resolver dependências DI (Spring)
A estrutura DI leva alguns segundos para inicializar antes que os testes possam começar. Isso retarda o ciclo de feedback, especialmente nos estágios iniciais de desenvolvimento.
Portanto, tento não usar DI em testes de integração, mas crio os objetos necessários manualmente e os "amarro" juntos. Se você estiver usando injeção de construtor, este é o mais fácil. Normalmente, em seus testes, você valida a lógica de negócios e não precisa de DI para isso.
Além disso, desde a versão 2.2, Spring Boot oferece suporte à inicialização lenta de beans, o que acelera significativamente os testes usando DI.
Seu código deve ser testável
Não use acesso estático. Nunca
O acesso estático é um anti-padrão. Primeiro, ele ofusca dependências e efeitos colaterais, tornando todo o código difícil de ler e sujeito a erros sutis. Em segundo lugar, o acesso estático atrapalha o teste. Você não pode mais substituir objetos, mas em testes você precisa usar simulações ou objetos reais com uma configuração diferente (por exemplo, um objeto DAO apontando para o banco de dados de teste).
Em vez de acessar o código estaticamente, coloque-o em um método não estático, instancie a classe e passe o objeto resultante para o construtor.
//
public class ProductController {
public List<ProductDTO> getProducts() {
List<ProductEntity> products = ProductDAO.getProducts();
return mapToDTOs(products);
}
}
//
public class ProductController {
private ProductDAO dao;
public ProductController(ProductDAO dao) {
this.dao = dao;
}
public List<ProductDTO> getProducts() {
List<ProductEntity> products = dao.getProducts();
return mapToDTOs(products);
}
}
Felizmente, frameworks de DI como Spring fornecem ferramentas que tornam o acesso estático desnecessário, criando e vinculando objetos automaticamente sem nosso envolvimento.
Parametrizar
Todas as partes relevantes da classe devem ser configuráveis do lado do teste. Essas configurações podem ser passadas para o construtor da classe.
Imagine, por exemplo, que seu DAO tem um limite fixo de 1000 objetos por solicitação. Para verificar esse limite, você precisará adicionar 1001 objetos ao banco de dados de teste antes do teste. Usando o argumento construtor, você pode tornar este valor personalizável: na produção, deixe 1000, no teste, reduza para 2. Assim, para verificar o trabalho do limite, você precisará apenas adicionar 3 registros ao banco de dados de teste.
Use injeção de construtor
A injeção de campo é nociva e leva a uma capacidade de teste ruim do código. Você precisa inicializar o DI antes dos testes ou fazer alguma mágica estranha de reflexão. Portanto, é preferível usar injeção de construtor para controlar facilmente objetos dependentes durante o teste.
Em Java, você precisa escrever um pequeno código extra:
//
public class ProductController {
private ProductDAO dao;
private TaxClient client;
public ProductController(ProductDAO dao, TaxClient client) {
this.dao = dao;
this.client = client;
}
}
Em Kotlin, a mesma coisa é escrita de forma muito mais concisa:
//
class ProductController(
private val dao: ProductDAO,
private val client: TaxClient
){
}
Não use Instant.now() ounew Date()
Você não precisa obter a hora atual por chamadas
Instant.now()ou new Date()no código de produção se quiser testar esse comportamento.
//
public class ProductDAO {
public void updateDateModified(String productId) {
Instant now = Instant.now(); // !
Update update = Update()
.set("dateModified", now);
Query query = Query()
.addCriteria(where("_id").eq(productId));
return mongoTemplate.updateOne(query, update, ProductEntity.class);
}
}
O problema é que o tempo gasto não pode ser controlado pelo teste. Você não conseguirá comparar o resultado obtido com um valor específico, pois é diferente o tempo todo. Use uma classe
Clockde Java em vez disso .
//
public class ProductDAO {
private Clock clock;
public ProductDAO(Clock clock) {
this.clock = clock;
}
public void updateProductState(String productId, State state) {
Instant now = clock.instant();
// ...
}
}
Neste teste, você pode criar um objeto simulado para
Clock, passá-lo ProductDAOe configurar o objeto simulado para retornar ao mesmo tempo. Após as chamadas, updateProductState()poderemos verificar se o valor que especificamos entrou no banco de dados.
Separe a execução assíncrona da lógica real
Testar código assíncrono é complicado. Bibliotecas como Awaitility são de grande ajuda, mas o processo ainda é complicado e podemos terminar com um teste intermitente. Faz sentido separar a lógica de negócios (geralmente síncrona) e o código de infraestrutura assíncrono, se possível.
Por exemplo, ao trazer a lógica de negócios para o ProductController, podemos testá-la facilmente de forma síncrona. Toda lógica assíncrona e paralela permanecerá no ProductScheduler, que pode ser testado isoladamente.
//
public class ProductScheduler {
private ProductController controller;
@Scheduled
public void start() {
CompletableFuture<String> usFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.US));
CompletableFuture<String> germanyFuture = CompletableFuture.supplyAsync(() -> controller.doBusinessLogic(Locale.GERMANY));
String usResult = usFuture.get();
String germanyResult = germanyFuture.get();
}
}
Kotlin
Meu artigo Práticas recomendadas para teste de unidade em Kotlin contém muitas técnicas de teste de unidade específicas do Kotlin. (Nota tradução: escreva nos comentários se você estiver interessado na tradução russa deste artigo).