Vamos aos conceitos trazidos por Freeman e Pryce (2010).
Exemplo de Critério de Aceite: Um Cliente pode pagar o cartão de crédito em determinadas bandeiras. (Valente, 2022)
Isto é, devemos fazer como CAIXA PRETA. Testes de Caixa Preta significam que interagimos somente com a parte externa (outside), através de:
É o teste mais desafiador. Exige que tenhamos todos os componentes mais básicos primeiro. É comumente ignorado, elevando-se os riscos de regressão de funcionalidades.
# language: pt
Funcionalidade: Como Cliente Eu gostaria de consultar produto para comprar
Como um Cliente a procura de um produto
Eu gostaria de saber se o produto está a venda
De modo que Eu possa ser encorajado a comprá-lo.
Criterios de Aceite:
1. Procura por um produto por categoria
2. Recebe um aviso ao procurar categoria indisponivel
Fundo:
Dados produtos cadastrados na loja
Delineacao do Cenario: Procura por um produto por categoria
Quando o cliente procurar por "<categoria>"
Entao uma lista de produtos deve aparecer com produtos pertencentes a "<categoria>"
E cada produto mostrado aparecera com "<nome>", "<quantidade>" e "<disponivelAVenda>"
É o simples fato de algo que funcionava e, depois de algumas alterações no sistema, deixou de funcionar. É onde mais nos aborrecemos enquanto usuários.
Podemos pegar nosso código e testar junto com os seguintes códigos externos:
Não. Em pequenas aplicações, quase sempre não precisaremos de Teste Integrado. Um de Aceitação já bastaria. Entretanto, Testes de Aceitação são mais lentos para verificar problemas de configuração. Aqui poderia fazer mais sentido o uso de testes integrados mais rápidos de serem executados.
@Test
@Sql(scripts = "insert-products.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "delete-products.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
void quantityByProductTypeTest() {
assertThat(stockService.getQuantityByProductType("Perfume")).isEqualTo(3L);
}
class StockService {
@Autowired
private ProductRepository productRepository;
public long getQuantityByProductType(String name){
return productRepository.countByProductType_Name(name);
}
}
interface ProductRepository extends JpaRepository<Product, UUID> {}
Fonte: Writing Integration Tests in Spring Boot App with JPA using JUnit5, Testcontainers and JPA Buddy
class StackTest {
@Test
public canCreateEmptyStack() {
var stack = new Stack();
assertTrue(stack.isEmpty());
}
}
Exemplos:
Uma Classe que cuida do parse de Datas e de URLs ao mesmo tempo. O que uma coisa tem a ver com outra?
Uma Classe que deveria processar URL mas só verifica partes de uma URL e não ela toda. Isso força a procura de outros processadores para complementar o trabalho que ela deveria fazer.
Só seguirmos a Golden Rule do TDD
Escreva um teste que irá falhar!
"The simplest thing that could possibly work" (Kent Beck)
Uncle Bob incentiva começarmos com Casos Degenerados (Clean Craftsmanship)
Entretanto, Freeman & Pryce afirmam que isso entrega pouco valor ao sistema. Não dão feedback interessante sobre a validade das ideias de negócio. Os Clientes ficam mais preocupados. Afeta a moral da equipe.
Entretanto, Anote os casos degenerados/falha num bloco de notas ou registre num Card de subtask. Caso esqueçamos, isso vai virar a futura Issue que o cliente vai descobrir.
(1) Como Cliente, Eu gostaria de consultar produto para comprar.
(2) Como Cliente, Eu gostaria de receber um aviso de que, ao procurar um produto por categoria indisponível, Eu receba uma mensagem me notificando.
É uma etapa de infraestrutura. Segundo Freeman & Pryce, não devemos contar isso nas iterações comuns. Após a conclusão da iteração zero, o projeto se inicia de fato.
Devemos procurar a arquitetura mais simples e ao mesmo tempo completa. Vai precisar de um Banco de Dados? Já o coloque. Vai precisar de uma API REST? Já a coloque.
O Agile orienta descobrir os detalhes a medida que as iterações avançam, o software é entregue e o feedback real é tomado.
skinparam actorStyle awesome
actor User
interface API
database "H2 Database" {
[Product]
}
API - [Web Application]
[Web Application] - [Product]
User <-> API
Chamamos Teste de Aceitação como UAT - User Acceptance Test também.
git clone https://github.com/xpian94/forkstore.git
mvn clean verify -P all-tests
mvn clean verify # ignora e2e
mvn clean verify -DskipTests # ignora e2e e ut, sobrando it
mvn clean verify -DskipITs # ignora e2e e it, sobrando ut
Aconselho a evitar este tipo de comando. Um e2e deve exercitar
todos os testes também. Algumas IDEs oferecem uma maneira de executar somente os testes integrados por meio de plugins, devendo ser usado com muita cautela.
git commit --allow-empty -m "#trigger ci" # pode usar qualquer string
git push
Abra o arquivo ClienteConsultaProdutoAComprar.feature
(pasta test/resources/org/forkstore
)
Funcionalidade: Como Cliente Eu gostaria de consultar produto para comprar
Como um Cliente a procura de um produto
Eu gostaria de saber se o produto esta a venda
De modo que Eu possa ser encorajado a compra-lo.
Criterios de Aceite:
1. Procura por um produto por categoria
2. Recebe um aviso ao procurar categoria indisponivel
Fundo:
Dados produtos cadastrados na loja
Delineacao do Cenario: Procura por um produto por categoria
Quando o cliente procurar por "<categoria>"
Entao uma lista de produtos deve aparecer com produtos pertencentes a "<categoria>"
E cada produto mostrado aparecera com "<nome>", "<quantidade>" e "<disponivelAVenda>"
Exemplos:
| categoria | nome | quantidade | disponivelAVenda |
| Utensilios | Kit 12 Facas | 3 | Sim |
| Utensilios | Colher de Silicone | 12 | Sim |
| Utensilios | Ralador 4 faces Inox | 0 | Nao |
Evite colocar acentuações em delineação de cenário e fundo.
ProcuraProdutoPorCategoria.java
no pacote org.forkstore.e2e
@Quando("o cliente procurar por {string}")
public void oClienteProcurarPor(String categoria) throws JsonProcessingException {
var url = "http://localhost:%d/product?category=%s".formatted(port, categoria);
response = restTemplate.getForObject(url, String.class);
responseAsJsonNode = objectMapper.readTree(response);
}
public class ProcuraProdutoPorCategoria {
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
private String response;
private final ObjectMapper objectMapper = new ObjectMapper();
private JsonNode responseAsJsonNode;
// ...
}
@Entao("uma lista de produtos deve aparecer com produtos pertencentes a {string}")
public void umaListaDeProdutosDeveAparecerComProdutosPertencentesA(String categoria) {
var fieldValues = responseAsJsonNode.findValues("category");
var fieldValuesAsList = fieldValues.stream().map(JsonNode::asText).toList();
assertThat(fieldValues.size()).isEqualTo(3);
assertThat(fieldValuesAsList).contains(categoria);
}
ProductController.java
em main/java/org/forkstore
(é o pacote org.forkstore
)import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@RestController
public class ProductController {
@GetMapping(value = "/product", produces = MediaType.APPLICATION_JSON_VALUE)
String searchByCategory(@RequestParam(name = "category") String category) {
return """
[
{
"category": "Utensilios",
"name": "Kit 12 Facas"
},
{
"category": "Utensilios",
"name": "Colher de Silicone"
},
{
"category": "Utensilios",
"name": "Ralador 4 faces Inox"
}
]
""";
}
}
@Entao("uma lista de produtos deve aparecer com produtos pertencentes a {string}")
public void umaListaDeProdutosDeveAparecerComProdutosPertencentesA(String categoria) {
findByFieldNameAndValueAndVerify("category", categoria);
}
private void findByFieldNameAndValueAndVerify(String fieldName, String fieldValue) {
var fieldValues = responseAsJsonNode.findValues(fieldName);
var fieldValuesAsList = fieldValues.stream().map(JsonNode::asText).toList();
assertThat(fieldValues.size()).isEqualTo(3);
assertThat(fieldValuesAsList).contains(fieldValue);
}
<nome>
, <quantidade>
e <disponivelAVenda>
@E("cada produto mostrado aparecera com {string}, {string} e {string}")
public void cadaProdutoMostradoApareceraComE(String nome, String quantidade, String disponivelAVenda) {
findByFieldNameAndValueAndVerify("name", nome);
findByFieldNameAndValueAndVerify("quantity", quantidade);
findByFieldNameAndValueAndVerify("available", String.valueOf(disponivelAVenda.equals("Sim")));
}
@GetMapping(value = "/product", produces = MediaType.APPLICATION_JSON_VALUE)
String searchByCategory(@RequestParam(name = "category") String category) {
return """
[
{
"category": "Utensilios",
"name": "Kit 12 Facas",
"quantity": 3,
"available": true
},
{
"category": "Utensilios",
"name": "Colher de Silicone",
"quantity": 12,
"available": true
},
{
"category": "Utensilios",
"name": "Ralador 4 faces Inox",
"quantity": 0,
"available": false
}
]
""";
}
A classe PreparacaoDaHistoria
fica em org.forkstore.e2e
public class PreparacaoDaHistoria {
@Dados("produtos cadastrados na loja")
public void produtosCadastradosNaLoja() {
var first = new Product();
var second = new Product();
var third = new Product();
}
}
package org.forkstore;
public class Product { }
public class PreparacaoDaHistoria {
@Dados("produtos cadastrados na loja")
public void produtosCadastradosNaLoja() {
var first = new Product();
var second = new Product();
var third = new Product();
first.setCategory("Utensilios");
first.setName("Kit 12 Facas");
first.setQuantity(3);
first.setAvailable(true);
}
}
second.setCategory("Utensilios");
second.setName("Colher de Silicone");
second.setQuantity(12);
second.setAvailable(true);
third.setCategory("Utensilios");
third.setName("Ralador 4 faces Inox");
third.setQuantity(0);
third.setAvailable(false);
public class PreparacaoDaHistoria {
@Dados("produtos cadastrados na loja")
public void produtosCadastradosNaLoja() {
// ...
repo.saveAll(List.of(first, second, third));
}
}
public class PreparacaoDaHistoria {
@Autowired
private ProductRepository repo;
// ...
}
@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {}
@Entity
public class Product {
Long id;
// ...
}
Teremos muitos erros ainda. Vamos corrigindo aos poucos.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
// ...
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
public void setCategory(String category) {}
public void setName(String name) {}
public void setQuantity(int quantity) {}
public void setAvailable(boolean available) {}
}
@RestController
public class ProductController {
private final ProductRepository repo;
public ProductController(ProductRepository repo) {
this.repo = repo;
}
@GetMapping(value = "/product", produces = MediaType.APPLICATION_JSON_VALUE)
List<Product> searchByCategory(@RequestParam(name = "category") String category) {
return (List<Product>) repo.findAll();
}
}
A refatoração aqui apontou que nossa fase de criação de dados de teste está falha.
Não tem problema, vamos consertá-la.
Product
Existe um recurso interessante de algumas IDEs chamado Multi-selection.
Podemos reaproveitar setters e transformar em campos. Deve ficar assim:
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Long id;
String category;
String name;
Integer quantity;
Boolean available;
}
// ...
import lombok.Setter;
@Entity
@Setter
public class Product {
// ...
}
Com isso, teremos setters sem precisar de utilizar recursos de IDE. O Lombok permite
gerar código no momento que o compilador gera a AST (Abstract Syntax Tree).
org.opentest4j.AssertionFailedError:
expected: 3
but was: 0
Expected :3
Actual :0
// ...
@Entity
@Setter
@Getter
public class Product {
// ...
}
org.opentest4j.AssertionFailedError:
expected: 3
but was: 6
Expected :3
Actual :6
org.opentest4j.AssertionFailedError:
expected: 3
but was: 9
Expected :3
Actual :9
Fundo
(Background
)Fundo:
Dados produtos cadastrados na loja
Isso faz com que cada Exemplo de um cenário delineado tenha a execução do método
PreparacaoDaHistoria.produtosCadastradosNaLoja()
. Isso vai poluindo a base de dados.
public class Hooks {
@Autowired
private ProductRepository repo;
@After // @Before aqui tem o mesmo efeito
public void cleanUp() {
repo.deleteAll();
}
}
After: quer dizer que o método
cleanUp
vai ser chamado depois do exemplo de teste de um cenário delineado tiver sido executado.
git push
, pull request
, ....github/.github_action.yml
temosname: CI
on:
pull_request:
branches:
- 'feature/*'
- 'release/*'
- 'main'
feature/*
, /release/*
e main
, algumas etapas serão executadas sequencialmente..github/workflows
. Definimos com a expressão runs-on
relativa ao job.jobs:
test: # test = job id
runs-on: ubuntu-latest # vai rodar um Ubuntu Linux padrão
git checkout
na branchjobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
uses
: vai executar uma Action como parte da step. Logo, o checkout será feito logo no início da step. Se estiver fazendo um commit e push na branch ABC
, é nesta branch que será feito o checkout.
jobs:
test:
runs-on: ubuntu-latest
steps:
# ...
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
cache: 'maven'
- name: CI Info
run: echo "${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
- name: Build and All Tests
run: mvn clean verify -P all-tests
- name: Build Tests (Unit and Integration)
if: startsWith(github.head_ref, 'feature')
run:
mvn clean verify -P ut-it
feature
, chame somente os testes unitários e integrados- name: Generate JaCoCo Badge
id: jacoco
uses: cicirello/jacoco-badge-generator@v2
with:
generate-branches-badge: true
.svg
como- name: Log coverage percentage
run: |
echo "coverage = ${{ steps.jacoco.outputs.coverage }}"
echo "branch coverage = ${{ steps.jacoco.outputs.branches }}"
- name: Upload JaCoCo coverage report
uses: actions/upload-artifact@v2
with:
name: jacoco-report
path: target/site/jacoco/
Error: Tests run: 5, Failures: 3, Errors: 0, Skipped: 0
Quais objetos podemos pensar? Do DDD (Domain-Driven Design), temos:
Os Casos de Uso oferecem maior facilidade. Podemos isolar os componentes externos mais facilmente através de Test Double (Dublê de Teste).
Alguns preferem ainda criar uma nova classe de Caso de Uso. Opto pela simplicidade para evitar proliferação de classes.
É o coração da nossa intenção. Queremos chamar o caso de uso da procura de um produto por categoria.
public class ProductServiceTest {
@Test
void nothing() {
var found = service.searchByCategory("category");
}
}
Vamos resolvendo as falhas...
public class ProductServiceTest {
private ProductService service;
// ...
}
public class ProductService {
public List<Object> searchByCategory(String category) {
return null;
}
}
Teremos a falha
java.lang.NullPointerException: Cannot invoke ...because "this.service" is null
public class ProductServiceTest {
private ProductService service = new ProductService();
@Test
void nothing() {
var found = service.searchByCategory("category");
}
}
public class ProductServiceTest {
private ProductService service = new ProductService();
@Test
void nothing() {
var found = service.searchByCategory("category");
assertThat(found.size()).isEqualTo(1);
}
}
Falha. Resolvendo...
public class ProductService {
public List<Object> searchByCategory(String category) {
return List.of(new Object());
}
}
Sucesso! BIZARRO?! Calma...
@Test
void nothing() {
var found = service.searchByCategory("category");
verify(repository).findByCategory("category");
assertThat(found.size()).isEqualTo(1);
}
public class ProductServiceTest {
private ProductService service = new ProductService();
private ProductRepository repository;
// ...
}
public class ProductRepository {
public void findByCategory(String category) {}
}
org.mockito.exceptions.misusing.NullInsteadOfMockException:
Argument passed to verify() should be a mock but is null!
Examples of correct verifications:
verify(mock).someMethod();
verify(mock, times(10)).someMethod();
verify(mock, atLeastOnce()).someMethod();
not: verify(mock.someMethod());
Also, if you use @Mock annotation don't miss openMocks()
A falha diz tudo. repository
precisa ser um Mock instanciado. Vamos instanciar então...
org.mockito.exceptions.misusing.NotAMockException:
Argument passed to verify() is of type ProductRepository and is not a mock!
...
repository
precisa ser um Mock!@ExtendWith(MockitoExtension.class) // Habilita injeção automática aos Mocks
public class ProductServiceTest {
private ProductService service = new ProductService();
@Mock
private ProductRepository repository;
@Test
void nothing() {
var found = service.searchByCategory("category");
verify(repository).findByCategory("category");
assertThat(found.size()).isEqualTo(1);
}
}
Wanted but not invoked:
repository.findByCategory("category");
-> at org.forkstore.ProductRepository.findByCategory(ProductRepository.java:5)
Actually, there were zero interactions with this mock.
Podemos aprender a programar assim, analisando respostas das falhas. findByCategory
nunca é chamado de fato.
Vamos fazer com que seja chamado forçadamente.
public class ProductService {
private ProductRepository repository;
public List<Object> searchByCategory(String category) {
repository.findByCategory(category);
return List.of(new Object());
}
}
Falha. Corrigindo.
public class ProductService {
private ProductRepository repository = new ProductRepository();
// ...
}
Vai falhar novamente.
Wanted but not invoked:
repository.findByCategory("category");
-> at org.forkstore.ProductRepository.findByCategory(ProductRepository.java:5)
Actually, there were zero interactions with this mock.
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock
private ProductRepository repository;
private ProductService service = new ProductService(repository);
// ...
}
public class ProductService {
private ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
// ...
}
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock
private ProductRepository repository;
// Injeção de dependência para o objeto service receber referencia do mock de repository
@InjectMocks
private ProductService service = new ProductService(repository);
// ...
}
Ou de forma abreviada
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
// ...
@InjectMocks
private ProductService service;
// ...
}
findByCategory
é chamado na regra de negócio.public List<Object> searchByCategory(String category) {
repository.findByCategory(category);
return List.of(new Object());
}
@Test
void nothing() {
var found = service.searchByCategory("category");
verify(repository).findByCategory("category");
assertThat(found.size()).isEqualTo(1);
found = service.searchByCategory("other");
verify(repository).findByCategory("other");
assertThat(found.size()).isEqualTo(2);
}
Agora vamos ter a falha que precisamos.
public List<Object> searchByCategory(String category) {
repository.findByCategory(category);
if (category.equals("category")) return List.of(new Object());
if (category.equals("other")) return List.of(new Object(), new Object());
return List.of(new Object());
}
Resolve, mas cria um problema: os ifs estão gerando duplicação. Vamos refatorar e
procurar uma forma mais inteligente de remover a duplicação.
E como a regra de negócio só tem feito 1 ação, já colete o seu retorno. Se porventura precisarmos de adicionar novas regras, é só reescrever.
public List<Object> searchByCategory(String category) {
return repository.findByCategory(category);
}
public void findByCategory(String category) {
if (category.equals("category")) return List.of(new Object());
if (category.equals("other")) return List.of(new Object(), new Object());
return List.of(new Object());
}
Teremos problemas de compilação. Vamos ajustar a assinatura do método.
public List<Object> findByCategory(String category) { /* ... */ }
Isso resolve a compilação. Entretanto, os testes falharam.
org.opentest4j.AssertionFailedError:
expected: 1
but was: 0
Expected :1
Actual :0
@ExtendWith(MockitoExtension.class)
public class ProductServiceTest {
@Mock
private ProductRepository repository;
@InjectMocks
private ProductService service;
@Test
void nothing() {
var found = service.searchByCategory("category");
// ...
}
}
service
vai receber a referência de um objeto anotado com @Mock
caso a utilize. E realmente vai ser usado, pois o construtor de ProductService
vai recebê-la.public ProductService(ProductRepository repository) {
this.repository = repository;
}
repository
anotado com @Mock
indica que o objeto do tipo ProductRepository
terá sua instanciação interceptada. Ela vai ser substituída por um outro objeto que vai atuar como um dublê.repository
será um Doppelganger.Java Reflection API
. As frameworks de Mock a utilizam.stubbing
@Test
void nothing() {
when(repository.findByCategory("category")).thenReturn(List.of(new Object()));
when(repository.findByCategory("other")).thenReturn(List.of(new Object(), new Object()));
var found = service.searchByCategory("category");
// ...
}
public class ProductRepository {
public List<Object> findByCategory(String category) {
return null;
}
}
@Test
void nothing() {
when(repository.findByCategory("category")).thenReturn(List.of(new Object()));
when(repository.findByCategory("other")).thenReturn(List.of(new Object(), new Object()));
var found = service.searchByCategory("category");
verify(repository).findByCategory("category");
assertThat(found.size()).isEqualTo(1);
found = service.searchByCategory("other");
verify(repository).findByCategory("other");
assertThat(found.size()).isEqualTo(2);
}
@Test
void nothing() {
String category = "category";
String other = "other";
List<Object> productsFromCategory = List.of(new Object());
List<Object> productsFromOther = List.of(new Object(), new Object());
stubbingFindByCategory(category, productsFromCategory);
stubbingFindByCategory(other, productsFromOther);
searchByCategoryAndAssert("category", 1);
searchByCategoryAndAssert("other", 2);
}
private void stubbingFindByCategory(String category, List<Object> products) {
when(repository.findByCategory(category)).thenReturn(products);
}
private void searchByCategoryAndAssert(String category, int expectedSize) {
var found = service.searchByCategory(category);
verify(repository).findByCategory(category);
assertThat(found.size()).isEqualTo(expectedSize);
}
@Test
void shouldSearchByCategory() { /* ... */ }
new Object()
? Calma, temos que ter testes específicos para nos criar a necessidade de mudar.repository
com JPA (Jakarta Persistence).public class ProductRepositoryIT {
@Test
void nothing() {
}
}
Vamos lá
public class ProductRepositoryIT {
private ProductRepository repository;
@Test
void nothing() {
var products = repository.findByCategory("category");
}
}
public class ProductRepositoryIT {
private ProductRepository repository = new ProductRepository();
// ...
}
@Test
void nothing() {
var products = repository.findByCategory("category");
Assertions.assertThat(products.get(0).getId()).isEqualTo(1L);
}
Falha. Como resolver? Vemos que a assinatura abaixo está nos tirando a possibilidade de definir o método getId()
.
public List<Object> findByCategory(String category) { /* ... */ }
Object
vai dar lugar à sua herdeira: Product
public class Product extends Object {}
// Ou melhor
public class Product {}
public List<Product> findByCategory(String category) {
return null;
}
public List<Object> searchByCategory(String category) {
return repository.findByCategory(category);
}
java: incompatible types: ...List<org.forkstore.Product> cannot be converted to ...List<java.lang.Object>
Simples. Mas isso vai gerar uma cascata de erros. Outros testes acabam sendo dependentes deste código. Vamos precisar consertá-los.
public List<Product> searchByCategory(String category) {
return repository.findByCategory(category);
}
private void stubbingFindByCategory(String category, List<Object> products) {
when(repository.findByCategory(category)).thenReturn(products);
}
transforme em
private void stubbingFindByCategory(String category, List<Product> products) {
when(repository.findByCategory(category)).thenReturn(products);
}
@Test
void shouldSearchByCategory() {
String category = "category";
String other = "other";
List<Product> productsFromCategory = List.of(new Product());
List<Product> productsFromOther = List.of(new Product(), new Product());
// ...
}
ProductRepositoryIT
.mvn clean verify -Dtest=ProductRepositoryIT
getId()
public class Product {
public Long getId() {
return 0L;
}
}
Long? Tradicionalmente em mapeamento Objeto-relacional, IDs podem ser muito grandes. Long é o suficiente e de melhor capacidade.
java.lang.NullPointerException: Cannot invoke "java.util.List.get(int)" because "products" is null
public List<Product> findByCategory(String category) {
return null;
}
Corrija
public List<Product> findByCategory(String category) {
return List.of(new Product());
}
org.opentest4j.AssertionFailedError:
expected: 1L
but was: 0L
Expected :1L
Actual :0L
public class Product {
public Long getId() {
return 1L;
}
}
Assertions.assertThat(products.get(1).getId()).isEqualTo(2L);
Compreensível
java.lang.IndexOutOfBoundsException: Index: 1 Size: 1
Rápido
public List<Product> findByCategory(String category) {
return List.of(new Product(), new Product());
}
org.opentest4j.AssertionFailedError:
expected: 2L
but was: 1L
Expected :2L
Actual :1L
public class Product {
private static Long id = 1L;
public Long getId() {
return id++;
}
}
O teste integrado tem a necessidade de testar um código externo fora de nosso controle. O teste que fizemos anteriormente é um Unitário ainda. Vamos transformar em Integrado.
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;
@Entity
@Getter
public class Product {
@Id
private Long id;
}
Assertions.assertThat(products.get(0).getId()).isEqualTo(1L);
Assertions.assertThat(products.get(1).getId()).isEqualTo(2L);
para
Product first = products.get(0);
Product second = products.get(1);
assertThat(first.getId()).isEqualTo(1L);
assertThat(second.getId()).isEqualTo(2L);
Fica mais nítido o problema. getId
retorna o valor do campo id
que é nulo. Long
não é um tipo primitivo, é um tipo chamado Wrapper Class. Ele "embala" um primitivo.
ProductRepository
num CrudRepository
.Vamos sair disto
public class ProductRepository {
public List<Product> findByCategory(String category) {
return List.of(new Product(), new Product());
}
}
Para isto
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByCategory(String name);
}
findByCategory()
que o JPA já tem pronta. É o reuso na sua maior expressão.public class ProductRepositoryIT {
private ProductRepository repository = new ProductRepository();
// ...
}
@Autowired
private ProductRepository repository;
@DataJpaTest
@DataJpaTest
public class ProductRepositoryIT { /* ... */ }
No property 'category' found for type 'Product'
@Entity
@Getter
public class Product {
@Id
private Long id;
private String category;
}
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
Product
está vazia. Só a gente acionar a persistência.@Test
void nothing() {
repository.saveAll(List.of(new Product(), new Product()));
}
Falha.
...JpaSystemException: Identifier of entity '...Product' must be manually assigned before calling 'persist()'
@Id
@GeneratedValue // automatiza a geração de ID
private Long id;
Voltamos ao
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
category
não está sendo cadastrado no banco@Test
void nothing() {
Product one = Product.builder()
.category("category")
.build();
Product two = Product.builder()
.category("category")
.build();
repository.saveAll(List.of(one, two));
// ...
}
@Entity
@Getter
@Builder // AQUI!
@NoArgsConstructor // VAI PRECISAR PARA NÃO QUEBRAR OUTROS TESTES
@AllArgsConstructor // VAI PRECISAR
public class Product {
@Id
@GeneratedValue
private Long id;
private String category;
}
@Test
void shouldFindByCategory() {
var one = Product.builder()
.category("category")
.build();
var two = Product.builder()
.category("category")
.build();
repository.saveAll(List.of(one, two));
var products = repository.findByCategory("category");
one = products.get(0);
two = products.get(1);
assertThat(one.getId()).isEqualTo(1L);
assertThat(two.getId()).isEqualTo(2L);
}
Hibernate: select next value for product_seq
Hibernate: select next value for product_seq
Hibernate: insert into product (category,id) values (?,?)
Hibernate: insert into product (category,id) values (?,?)
Hibernate: select p1_0.id,p1_0.category from product p1_0 where p1_0.category=?
route
e um verbo (GET
, POST
, PUT
, ...).@SpringBootTest
public class ProductControllerIT {
@Test
void nothing() {
var response = mockMvc
.perform(get("/product").queryParam("category", "acategory"));
}
}
@SpringBootTest
public class ProductControllerIT {
@Autowired
private MockMvc mockMvc;
}
No qualifying bean of type '...MockMvc' available: expected at least 1 bean which qualifies as autowire candidate. ...
@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerIT { /* ... */ }
var response = mockMvc
.perform(get("/product").queryParam("category", "acategory"))
.andExpect(status().isOk());
java.lang.AssertionError: Status expected:<200> but was:<404>
Expected :200
Actual :404
@RestController
public class ProductController {
@RequestMapping(method = RequestMethod.GET, value = "/product")
void searchByCategory() {}
}
application/json
.var response = mockMvc
.perform(get("/product").queryParam("category", "acategory"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE));
@RequestMapping(method = RequestMethod.GET, value = "/product", produces = MediaType.APPLICATION_JSON_VALUE)
void searchByCategory() { /* ... */ }
Isso ainda não resolve. Mesmo erro. Mas é fácil resolver.
@RequestMapping(method = RequestMethod.GET, /* ... */)
String searchByCategory() {
return "";
}
assertThat(response.getRequest().getParameter("category")).isEqualTo("acategory");
// ...
String searchByCategory(@RequestParam(name = "category") String category) {
return "";
}
@Test
void shouldSearchByCategory_Ok() throws Exception {
var queryParamName = "category";
var queryParamValue = "acategory";
var response = mockMvc
.perform(get("/product").queryParam(queryParamName, queryParamValue))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
.andReturn();
assertThat(response.getRequest().getParameter(queryParamName)).isEqualTo(queryParamValue);
}
Step failed
java.lang.IllegalArgumentException: argument "content" is null
List<Product> searchByCategory(@RequestParam(name = "category") String category) {
return List.of(Product.builder().build());
}
Step failed
Expected :3
Actual :1
List<Product> searchByCategory(@RequestParam(name = "category") String category) {
Product one = Product.builder().build();
Product two = Product.builder().build();
Product three = Product.builder().build();
return List.of(one, two, three);
}
Ainda não, mas melhorou.
Step failed
java.lang.AssertionError:
Expecting ListN:
["null", "null", "null"]
to contain:
["Utensilios"]
but could not find the following element(s):
["Utensilios"]
ProductService
e chamar o método searchByCategory
?List<Product> searchByCategory(@RequestParam(name = "category") String category) {
return service.searchByCategory(category);
}
e
@RestController
public class ProductController {
@Autowired
private ProductService service;
// ...
}
Step failed
Expected :3
Actual :0
public class PreparacaoDaHistoria {
@Autowired
private ProductRepository repository;
@Dados("produtos cadastrados na loja")
public void produtosCadastradosNaLoja() {
var first = Product.builder()
.category("Utensilios")
.build();
var second = Product.builder()
.category("Utensilios")
.build();
var third = Product.builder()
.category("Utensilios")
.build();
repository.saveAll(List.of(first, second, third));
}
}
var first = Product.builder()
.category("Utensilios")
.name("Kit 12 Facas")
.quantity(3)
.available(true)
.build();
var second = Product.builder()
.category("Utensilios")
.name("Colher de Silicone")
.quantity(12)
.available(true)
.build();
var third = Product.builder()
.category("Utensilios")
.name("Ralador 4 faces Inox")
.quantity(0)
.available(false)
.build();
public class Product {
@Id
@GeneratedValue
private Long id;
private String category;
private String name;
private Integer quantity;
private Boolean available;
}
public class Hooks {
@Autowired
private ProductRepository productRepository;
@After
public void cleanUp() {
productRepository.deleteAll();
}
}
POI.