Você já teve essa condição?
Quero mostrar como o TDD pode melhorar a qualidade do código usando um exemplo específico.
Porque tudo o que conheci enquanto estudava a questão era bastante teórico.
Por acaso, escrevi duas aplicações quase idênticas: uma foi escrita no estilo clássico, já que eu não conhecia o TDD e a segunda - apenas usando o TDD.
Abaixo vou mostrar onde estavam as maiores diferenças.
Pessoalmente, isso era importante para mim, porque toda vez que alguém encontrava um bug no meu código, eu pegava um pesado sinal de menos por auto-estima. Sim, entendi que os bugs são normais, todos os escrevem, mas o sentimento de inferioridade não desapareceu. Além disso, no processo de evolução do serviço, às vezes percebi que eu mesmo escrevi um desses que coça minhas mãos para jogar tudo fora e reescrevê-lo novamente. E como isso aconteceu é incompreensível. De alguma forma, tudo estava bem no começo, mas depois de alguns recursos e depois de um tempo você não consegue ver a arquitetura sem lágrimas. Embora pareça que cada passo da mudança foi lógico. O sentimento de que eu não gostei do produto do meu próprio trabalho fluiu suavemente para o sentimento de que o programador era meu, com licença, como uma bala de merda.
Acabou que eu não sou o único e muitos dos meus colegas têm sensações semelhantes. E então decidi que aprenderia a escrever normalmente ou que estava na hora de mudar de profissão. Tentei o desenvolvimento orientado a testes na tentativa de mudar algo na minha abordagem de programação.
Olhando para o futuro, com base nos resultados de vários projetos, posso dizer que o TDD fornece uma arquitetura mais limpa, mas retarda o desenvolvimento. E nem sempre é adequado e nem para todos.
O que é TDD novamente
TDD - desenvolvimento através de testes. Artigo da Wiki
aqui .
A abordagem clássica é primeiro escrever um aplicativo e cobri-lo com testes.
Abordagem TDD - primeiro escrevemos testes para a classe e depois implementamos. Percorremos os níveis de abstração - do mais alto para o aplicado, ao mesmo tempo dividindo o aplicativo em camadas de classe, das quais
ordenamos o comportamento de que precisamos, livre de uma implementação específica.
E se eu lesse isso pela primeira vez, também não entenderia nada.
Muitas palavras abstratas: vejamos um exemplo.
Escreveremos um aplicativo real em Java, escreveremos em TDD e tentarei mostrar meu processo de pensamento durante o processo de desenvolvimento e, no final, tirar conclusões se faz sentido passar algum tempo com TDD ou não.
Tarefa prática
Suponha que tenhamos tanta sorte que tenhamos o sumário do que precisamos desenvolver. Normalmente, os analistas não se preocupam com isso, e é algo parecido com isto:
É necessário desenvolver um microsserviço que calcule a possibilidade de venda de mercadorias com entrega subsequente ao cliente em casa. As informações sobre esse recurso devem ser enviadas para um sistema DATA de terceiros.A lógica de negócios é a seguinte: um item está disponível para venda com entrega se:
- O produto está em estoque
- O contratado (por exemplo, a empresa DostavchenKO) tem a oportunidade de levá-lo ao cliente
- Cor do produto - não azul (não gostamos de azul)
Nosso microsserviço será notificado sobre uma alteração na quantidade de mercadorias na prateleira da loja por meio de uma solicitação http.
Esta notificação é um gatilho para calcular a disponibilidade.
Além disso, para que a vida não pareça ser mel:
- O usuário deve poder desativar manualmente certos produtos.
- Para não enviar spam aos DADOS, você só precisa enviar dados de disponibilidade para os produtos que foram alterados.
Lemos algumas vezes TK - e partimos.
Teste de integração
No TDD, uma das perguntas mais importantes que você deve fazer para tudo o que escreve é: "O que eu quero de ...?"
E a primeira pergunta que fazemos é apenas para toda a aplicação.
Então a questão é:
O que eu quero do meu microsserviço?A resposta é:
Na verdade, muitas coisas. Mesmo essa lógica simples oferece muitas opções, uma tentativa de escrever quais, e mais ainda, criar testes para todos eles, pode ser uma tarefa impossível. Portanto, para responder à pergunta no nível do aplicativo, escolheremos apenas os principais casos de teste.
Ou seja, assumimos que todos os dados de entrada são de formato válido, os sistemas de terceiros respondem normalmente e, anteriormente, não havia informações sobre o produto.
Então, eu quero:- Chegou um evento em que não há produtos na prateleira. Notifique que a entrega não está disponível.
- Chegou o evento em que o produto amarelo está em estoque, a DostavchenKO está pronta para recebê-lo. Notificar sobre a disponibilidade de mercadorias.
- Duas mensagens foram seguidas - ambas com uma quantidade positiva de mercadorias na loja. Enviou apenas uma mensagem.
- Chegaram duas mensagens: no primeiro, há um produto na loja, no segundo - ele não está mais lá. Enviamos duas mensagens: primeiro - disponível, depois - não.
- Posso desativar o produto manualmente e as notificações não são mais enviadas.
- ...
O principal aqui é parar no tempo: como eu já escrevi, há muitas opções, e não faz sentido descrever todas elas aqui - apenas as
mais básicas. No futuro, quando escrevermos testes para a lógica de negócios, é provável que a combinação deles cubra tudo o que for apresentado aqui. A principal motivação aqui é ter certeza de que, se esses testes forem aprovados, o aplicativo funcionará conforme necessário.
Toda essa lista de desejos agora será destilada em testes. Além disso, como essa é a lista de desejos no nível do aplicativo, teremos testes com o aumento do contexto da primavera, ou seja, bastante pesado.
E isso, infelizmente, para muitos fins de TDD, porque para escrever um teste de integração, você precisa de muito esforço que as pessoas nem sempre estão dispostas a gastar. E sim, esta é a etapa mais difícil, mas, acredite, depois de executá-la, o código quase se escreverá e você terá certeza de que seu aplicativo funcionará da maneira que você deseja.
No processo de responder à pergunta, você já pode começar a escrever código na classe spring initializr gerada. Os nomes dos testes são apenas nossa lista de desejos. Por enquanto, basta criar métodos vazios:
@Test public void notifyNotAvailableIfProductQuantityIsZero() {} @Test public void notifyAvailableYellowProductIfPositiveQuantityAndDostavchenkoApproved() {} @Test public void notifyOnceOnSeveralEqualProductMessages() {} @Test public void notifyFirstAvailableThenNotIfProductQuantityMovedFromPositiveToZero() {} @Test public void noNotificationOnDisabledProduct() {}
Com relação à nomeação dos métodos: eu recomendo fortemente que você os torne informativos, em vez de test1 (), test2 (), porque mais tarde, quando você esquecer a classe que você escreveu e o que é responsável, você terá a oportunidade em vez de tente analisar diretamente o código, basta abrir o teste e ler o método de contrato que a classe satisfaz.
Comece a preencher os testes
A idéia principal é emular tudo externo para verificar o que está acontecendo lá dentro.
“Externo” em relação ao nosso serviço é tudo o que NÃO é o próprio microsserviço, mas que se comunica diretamente com ele.
Nesse caso, o externo é:
- O sistema que nosso serviço notificará sobre alterações na quantidade de mercadorias
- Cliente que desconectará mercadorias manualmente
- Sistema DostavchenKO de terceiros
Para emular os pedidos dos dois primeiros, usamos MockMvc.
Para emular o DostavchenKO, usamos wiremock ou MockRestServiceServer.
Como resultado, nosso teste de integração se parece com o seguinte:
Teste de integração @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @AutoConfigureWireMock(port = 8090) public class TddExampleApplicationTests { @Autowired private MockMvc mockMvc; @Before public void init() { WireMock.reset(); } @Test public void notifyNotAvailableIfProductQuantityIsZero() throws Exception { stubNotification(
O que aconteceu?
Escrevemos um teste de integração, cuja passagem nos garante a operacionalidade do sistema de acordo com as principais histórias de usuários. E fizemos isso ANTES de começar a implementar o serviço.
Uma das vantagens dessa abordagem é que, durante o processo de escrita, tive que ir ao DostavchenKO
real e obter uma resposta
real a partir da solicitação
real que fizemos em nosso esboço. É muito bom que tenhamos resolvido isso no início do desenvolvimento, e não depois de todo o código estar escrito. E aqui acontece que o formato não é o especificado no TOR, ou o serviço geralmente não está disponível ou outra coisa.
Eu também gostaria de observar que não apenas não escrevemos
uma única linha de código que posteriormente será lançada no produto, como ainda não assumimos
uma única suposição sobre como nosso microsserviço será organizado dentro: quais camadas haverá, se usamos a base, se sim, qual, etc. No momento da redação do teste, somos abstraídos da implementação e, como veremos mais adiante, isso pode fornecer várias vantagens arquiteturais.
Ao contrário do TDD canônico, onde a implementação é gravada imediatamente após o teste, o teste de integração não leva muito tempo. De fato, ele não ficará verde até o final do desenvolvimento, até que absolutamente tudo esteja escrito, incluindo os arquivos.
Nós estamos indo além.
Controlador
Depois que escrevemos o teste de integração e agora estamos confiantes de que, após passarmos pela tarefa, poderemos dormir em paz à noite, é hora de começar a programar as camadas. E a primeira camada que iremos implementar é o controlador. Por que exatamente ele? Porque este é o ponto de entrada para o programa. Precisamos passar de cima para baixo, da primeira camada com a qual o usuário irá interagir, até a última.
Isso é importante.
E, novamente, tudo começa com a mesma pergunta:
O que eu quero do controlador?A resposta é:
Sabemos que o controlador está envolvido em comunicação com o usuário, validação e conversão de dados de entrada e não contém lógica de negócios. Portanto, a resposta para esta pergunta pode ser algo como isto:
Eu quero:- BAD_REQUEST retornou ao usuário ao tentar desconectar um produto com um ID inválido
- BAD_REQUEST ao tentar notificar sobre uma alteração de mercadorias com ID inválido
- BAD_REQUEST ao tentar notificar uma quantidade negativa
- INTERNAL_SERVER_ERROR se DostavchenKO estiver indisponível
- INTERNAL_SERVER_ERROR, se não for possível enviar para DATA
Como queremos ser amigáveis, para todos os itens acima, além do código http, é necessário exibir uma mensagem personalizada descrevendo o problema para que o usuário entenda qual é o problema.
- 200 se o processamento foi bem-sucedido
- INTERNAL_SERVER_ERROR com uma mensagem padrão em todos os outros casos, para não brilhar no stackrace
Até começar a escrever no TDD, a última coisa em que pensava era o que meu sistema traria para o usuário em um caso especial e, à primeira vista, improvável. Eu não pensei por uma razão simples: escrever uma implementação é tão difícil, a fim de levar em conta absolutamente todos os casos extremos, às vezes não há RAM suficiente no cérebro. E após a implementação escrita, analisar o código para algo que você pode não ter considerado com antecedência ainda é um prazer: todos pensamos que estamos escrevendo o código perfeito imediatamente). Embora não haja implementação, não há necessidade de pensar a respeito e, se houver, não há problema em alterá-la. Depois de escrever o teste primeiro, você não precisa esperar até que as estrelas converjam e, após a retirada para o produto, um certo número de sistemas falhará e o cliente o procurará com uma solicitação para corrigir alguma coisa. E isso se aplica não apenas ao controlador.
Comece a escrever testes
Tudo fica claro com os três primeiros: usamos a validação de primavera, se uma solicitação inválida chegar, o aplicativo lançará uma exceção, que capturaremos em um manipulador de exceções. Aqui, como eles dizem, tudo funciona por si só, mas como o controlador sabe que algum sistema de terceiros não está disponível?
É claro que o próprio controlador não deve saber nada sobre sistemas de terceiros, porque qual sistema perguntar e qual é a lógica de negócios, ou seja, deve haver algum tipo de intermediário. Esse intermediário é o serviço. E escreveremos testes no controlador usando a simulação deste serviço, simulando seu comportamento em certos casos. Portanto, o serviço deve, de alguma forma, informar ao controlador que o sistema está indisponível. Você pode fazer isso de maneiras diferentes, mas a maneira mais fácil de executar a execução personalizada. Escreveremos um teste para esse comportamento do controlador.
Teste de erro de comunicação com um sistema DATA de terceiros @RunWith(SpringRunner.class) @WebMvcTest @AutoConfigureMockMvc public class ControllerTest { @MockBean private UpdateProcessorService updateProcessorService; @Test public void returnServerErrorOnDataCommunicationError() throws Exception { doThrow(new DataCommunicationException()).when(updateProcessorService).processUpdate(any(Update.class)); performUpdate(
Nesta fase, várias coisas apareceram por si mesmas:
- Um serviço que será injetado no controlador e ao qual será delegado o processamento de uma mensagem recebida para uma nova quantidade de mercadorias.
- O método deste serviço e, consequentemente, sua assinatura, que conduzirá esse processamento.
- A percepção de que o método deve lançar execução personalizada quando o sistema não estiver disponível.
- Essa execução personalizada em si.
Por que sozinhos? Porque, como você se lembra, ainda não escrevemos uma implementação. E todas essas entidades apareceram no processo de como programamos os testes. Para que o compilador não jure, em código real, teremos que criar tudo descrito acima. Felizmente, quase qualquer IDE nos ajudará a gerar as entidades necessárias. Assim, meio que escrevemos um teste - e o aplicativo é preenchido com classes e métodos.
No total, os testes para o controlador são os seguintes:
Testes @RunWith(SpringRunner.class) @WebMvcTest @AutoConfigureMockMvc public class ControllerTest { @InjectMocks private Controller controller; @MockBean private UpdateProcessorService updateProcessorService; @Autowired private MockMvc mvc; @Test public void returnBadRequestOnDisableWithInvalidProductId() throws Exception { mvc.perform( post("/disableProduct?productId=-443") ).andDo( print() ).andExpect( status().isBadRequest() ).andExpect( content().json(getInvalidProductIdJsonContent()) ); } @Test public void returnBadRequestOnNotifyWithInvalidProductId() throws Exception { performUpdate(
Agora podemos escrever a implementação e garantir que todos os testes sejam aprovados com êxito:
Implementação @RestController @AllArgsConstructor @Validated @Slf4j public class Controller { private final UpdateProcessorService updateProcessorService; @PostMapping("/product-quantity-update") public void updateQuantity(@RequestBody @Valid Update update) { updateProcessorService.processUpdate(update); } @PostMapping("/disableProduct") public void disableProduct(@RequestParam("productId") @Min(0) Long productId) { updateProcessorService.disableProduct(Long.valueOf(productId)); } }
Manipulador de exceção @ControllerAdvice @Slf4j public class ApplicationExceptionHandler { @ExceptionHandler(ConstraintViolationException.class) @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse onConstraintViolationException(ConstraintViolationException exception) { log.info("Constraint Violation", exception); return new ErrorResponse(exception.getConstraintViolations().stream() .map(constraintViolation -> new ErrorResponse.Message( ((PathImpl) constraintViolation.getPropertyPath()).getLeafNode().toString() + " is invalid")) .collect(Collectors.toList())); } @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody @ResponseStatus(value = HttpStatus.BAD_REQUEST) public ErrorResponse onMethodArgumentNotValidException(MethodArgumentNotValidException exception) { log.info(exception.getMessage()); List<ErrorResponse.Message> fieldErrors = exception.getBindingResult().getFieldErrors().stream() .map(fieldError -> new ErrorResponse.Message(fieldError.getField() + " is invalid")) .collect(Collectors.toList()); return new ErrorResponse(fieldErrors); } @ExceptionHandler(DostavchenkoException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onDostavchenkoCommunicationException(DostavchenkoException exception) { log.error("DostavchenKO communication exception", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message("DostavchenKO communication exception"))); } @ExceptionHandler(DataCommunicationException.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onDataCommunicationException(DataCommunicationException exception) { log.error("DostavchenKO communication exception", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message("Can't communicate with Data system"))); } @ExceptionHandler(Exception.class) @ResponseBody @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse onException(Exception exception) { log.error("Error while processing", exception); return new ErrorResponse(Collections.singletonList( new ErrorResponse.Message(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()))); } }
O que aconteceu?
No TDD, você não precisa manter todo o código em sua cabeça.Vamos novamente: não mantenha toda a arquitetura na RAM. Basta olhar para uma camada. Ele é simples.
No processo usual, o cérebro não é suficiente, porque há muitas implementações. Se você é um super-herói que pode levar em consideração todas as nuances de um grande projeto em sua cabeça, o TDD não é necessário. Não sei como. Quanto maior o projeto, mais eu me engano.
Depois de perceber que você precisa entender apenas o que a próxima camada precisa, a iluminação ganha vida. O fato é que essa abordagem permite que você não faça coisas desnecessárias. Aqui você está conversando com uma garota. Ela diz algo sobre um problema no trabalho. E você pensa em como resolvê-lo, você destrói seu cérebro. E ela não precisa resolver, ela só precisa contar. E é isso. Ela só queria compartilhar algo. Aprender sobre isso no primeiro estágio de listen () não tem preço. Para todo o resto ... bem, você sabe.
Serviço
Em seguida, implementamos o serviço.
O que queremos do serviço?Queremos que ele lide com a lógica de negócios, ou seja:
- Ele sabia como desconectar mercadorias e também notificou sobre :
- A disponibilidade, se o produto não estiver desconectado, está em estoque, a cor do produto é amarela e o DostavchenKO está pronto para fazer a entrega.
- Inacessibilidade, se a mercadoria não estiver disponível, independentemente de qualquer coisa.
- Inacessibilidade, se o produto for azul.
- Inacessibilidade se o DostavchenKO se recusar a carregá-lo.
- Inacessibilidade se as mercadorias forem desconectadas manualmente.
- Em seguida, queremos que o serviço execute a execução se algum dos sistemas estiver indisponível.
- Além disso, para não enviar spam aos DADOS, você precisa organizar mensagens de envio preguiçosas, a saber:
- Se costumávamos enviar mercadorias disponíveis para mercadorias e agora calculamos o que está disponível, não enviamos nada.
- E se não estiver disponível antes, mas agora estiver disponível, enviamos.
- E você precisa anotá-lo em algum lugar ...
PARE!Você não acha que nosso serviço está começando a fazer muito?
A julgar pela nossa lista de desejos, ele sabe como desativar as mercadorias, considera a acessibilidade e garante que não envia mensagens enviadas anteriormente. Isso não é alta coesão. É necessário mover funcionalidades heterogêneas para diferentes classes e, portanto, já devem existir três serviços: um trata da desconexão de mercadorias, o outro calcula a possibilidade de entrega e a repassa para um serviço que decide se deve enviá-lo ou não. A propósito, dessa maneira, o serviço de lógica de negócios não saberá nada sobre o sistema DATA, que também é uma vantagem definitiva.
Na minha experiência, muitas vezes, depois de ter sido precipitado na implementação, é fácil ignorar os momentos arquitetônicos. Se escrevêssemos o serviço imediatamente, sem pensar no que deveria fazer e, mais importante, do que NÃO, a probabilidade de áreas de responsabilidade sobrepostas aumentaria. Gostaria de acrescentar em meu próprio nome que foi esse exemplo que me aconteceu na prática real e a diferença qualitativa entre os resultados do TDD e as abordagens de programação seqüencial que me inspiraram a escrever este post.
Lógica de negócios
Pensando no serviço de lógica de negócios pelos mesmos motivos que a alta coesão, entendemos que precisamos de mais um nível de abstração entre ele e o DostavchenKO real. E, como projetamos o serviço
primeiro , podemos exigir do cliente DostavchenKO um contrato interno que desejemos. No processo de escrever um teste para a lógica de negócios, entenderemos o que queremos do cliente da seguinte assinatura:
public boolean isAvailableForTransportation(Long productId) {...}
No nível do serviço, não importa para nós como o verdadeiro DostavchenKO responde: no futuro, a tarefa do cliente de alguma forma obterá essas informações dele. Uma vez que pode ser simples, mas em algum momento será necessário fazer vários pedidos: no momento, somos abstraídos disso.
Queremos uma assinatura semelhante de um serviço que lide com mercadorias desconectadas:
public boolean isProductEnabled(Long productId) {...}
Portanto, as perguntas "O que eu quero do serviço de lógica de negócios?" Registradas nos testes são as seguintes:
Testes de Serviço @RunWith(MockitoJUnitRunner.class) public class UpdateProcessorServiceTest { @InjectMocks private UpdateProcessorService updateProcessorService; @Mock private ManualExclusionService manualExclusionService; @Mock private DostavchenkoClient dostavchenkoClient; @Mock private AvailabilityNotifier availabilityNotifier; @Test public void notifyAvailableIfYellowProductIsEnabledAndReadyForTransportation() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())).thenReturn(true); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), true))); } @Test public void notifyNotAvailableIfProductIsAbsent() { final Update testProduct = new Update(1L, 0L, "Yellow"); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(manualExclusionService); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsBlue() { final Update testProduct = new Update(1L, 10L, "Blue"); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(manualExclusionService); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsDisabled() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(false); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); verifyNoMoreInteractions(dostavchenkoClient); } @Test public void notifyNotAvailableIfProductIsNotReadyForTransportation() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())).thenReturn(false); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); verify(availabilityNotifier, only()).notify(eq(new ProductAvailability(testProduct.getProductId(), false))); } @Test(expected = DostavchenkoException.class) public void throwCustomExceptionIfDostavchenkoCommunicationFailed() { final Update testProduct = new Update(1L, 10L, "Yellow"); when(dostavchenkoClient.isAvailableForTransportation(testProduct.getProductId())) .thenThrow(new RestClientException("Something's wrong")); when(manualExclusionService.isProductEnabled(testProduct.getProductId())).thenReturn(true); updateProcessorService.processUpdate(testProduct); } }
:
:
@RequiredArgsConstructor @Service @Slf4j public class UpdateProcessorService { private final AvailabilityNotifier availabilityNotifier; private final DostavchenkoClient dostavchenkoClient; private final ManualExclusionService manualExclusionService; public void processUpdate(Update update) { if (update.getProductQuantity() <= 0) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } if ("Blue".equals(update.getColor())) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } if (!manualExclusionService.isProductEnabled(update.getProductId())) { availabilityNotifier.notify(getNotAvailableProduct(update.getProductId())); return; } try { final boolean availableForTransportation = dostavchenkoClient.isAvailableForTransportation(update.getProductId()); availabilityNotifier.notify(new ProductAvailability(update.getProductId(), availableForTransportation)); } catch (Exception exception) { log.warn("Problems communicating with DostavchenKO", exception); throw new DostavchenkoException(); } } private ProductAvailability getNotAvailableProduct(Long productId) { return new ProductAvailability(productId, false); } }
TDD — . , :
public void disableProduct(long productId)
.
:
, - , :
- -, , , - , . . . , , , , . , , , . , , : , .
- -, . — , . , , , , , , . . ProductAvailability. , . . ., , god object, , , , TDD, , . , , «» — : « ...» , , TDD, .
:
@SpringBootTest @RunWith(SpringRunner.class) public class ManualExclusionServiceTest { @Autowired private ManualExclusionService service; @Autowired private ManualExclusionRepository manualExclusionRepository; @Before public void clearDb() { manualExclusionRepository.deleteAll(); } @Test public void disableItem() { Long productId = 100L; service.disableProduct(productId); assertThat(service.isProductEnabled(productId), is(false)); } @Test public void returnEnabledIfProductWasNotDisabled() { assertThat(service.isProductEnabled(100L), is(true)); assertThat(service.isProductEnabled(200L), is(true)); } }
@Service @AllArgsConstructor public class ManualExclusionService { private final ManualExclusionRepository manualExclusionRepository; public boolean isProductEnabled(Long productId) { return !manualExclusionRepository.exists(productId); } public void disableProduct(long productId) { manualExclusionRepository.save(new ManualExclusion(productId)); } }
, , , DATA .
, -, . . ProductAvailability, : productId isAvailable.
, :
- .
- , .
- , .
- , , , .
- DATA DataCommunicationException.
, :
, , , , .
ProductAvailability , . . , , . — @Document ( MongoDb) ProductAvailability.
, ProductAvailability , , , . , . . .
.
, , ProductAvailability, , , , . , ProductAvailability god object , : , , , .
@RunWith(SpringRunner.class) @SpringBootTest public class LazyAvailabilityNotifierTest { @Autowired private LazyAvailabilityNotifier lazyAvailabilityNotifier; @MockBean @Qualifier("dataClient") private AvailabilityNotifier availabilityNotifier; @Autowired private AvailabilityRepository availabilityRepository; @Before public void clearDb() { availabilityRepository.deleteAll(); } @Test public void notifyIfFirstTime() { sendNotificationAndVerifyDataBase(new ProductAvailability(1L, false)); } @Test public void notifyIfAvailabilityChanged() { final ProductAvailability oldProductAvailability = new ProductAvailability(1L, false); sendNotificationAndVerifyDataBase(oldProductAvailability); final ProductAvailability newProductAvailability = new ProductAvailability(1L, true); sendNotificationAndVerifyDataBase(newProductAvailability); } @Test public void doNotNotifyIfAvailabilityDoesNotChanged() { final ProductAvailability productAvailability = new ProductAvailability(1L, false); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); sendNotificationAndVerifyDataBase(productAvailability); verify(availabilityNotifier, only()).notify(eq(productAvailability)); } @Test public void doNotSaveIfSentWithException() { doThrow(new RuntimeException()).when(availabilityNotifier).notify(anyObject()); boolean exceptionThrown = false; try { availabilityNotifier.notify(new ProductAvailability(1L, false)); } catch (RuntimeException exception) { exceptionThrown = true; } assertTrue("Exception was not thrown", exceptionThrown); assertThat(availabilityRepository.findAll(), hasSize(0)); } @Test(expected = DataCommunicationException.class) public void wrapDataException() { doThrow(new RestClientException("Something wrong")).when(availabilityNotifier).notify(anyObject()); lazyAvailabilityNotifier.notify(new ProductAvailability(1L, false)); } private void sendNotificationAndVerifyDataBase(ProductAvailability productAvailability) { lazyAvailabilityNotifier.notify(productAvailability); verify(availabilityNotifier).notify(eq(productAvailability)); assertThat(availabilityRepository.findAll(), hasSize(1)); assertThat(availabilityRepository.findAll().get(0), hasProperty("productId", is(productAvailability.getProductId()))); assertThat(availabilityRepository.findAll().get(0), hasProperty("availability", is(productAvailability.isAvailable()))); } }
@Component @AllArgsConstructor @Slf4j public class LazyAvailabilityNotifier implements AvailabilityNotifier { private final AvailabilityRepository availabilityRepository; private final AvailabilityNotifier availabilityNotifier; @Override public void notify(ProductAvailability productAvailability) { final AvailabilityPersistenceObject persistedProductAvailability = availabilityRepository .findByProductId(productAvailability.getProductId()); if (persistedProductAvailability == null) { notifyWith(productAvailability); availabilityRepository.save(createObjectFromProductAvailability(productAvailability)); } else if (persistedProductAvailability.isAvailability() != productAvailability.isAvailable()) { notifyWith(productAvailability); persistedProductAvailability.setAvailability(productAvailability.isAvailable()); availabilityRepository.save(persistedProductAvailability); } } private void notifyWith(ProductAvailability productAvailability) { try { availabilityNotifier.notify(productAvailability); } catch (RestClientException exception) { log.error("Couldn't notify", exception); throw new DataCommunicationException(); } } private AvailabilityPersistenceObject createObjectFromProductAvailability(ProductAvailability productAvailability) { return new AvailabilityPersistenceObject(productAvailability.getProductId(), productAvailability.isAvailable()); } }
Conclusão
. , TDD, , , , ( , - ).
, , . , TDD , , .
, , , , , , . , , «» , , , - - , .
, TDD , , . , , TDD, , - , , TDD, , .
, .
. , , , , , , TDD .
, , TDD.
, json. , , json POJO-. IDEA, , JSON.
?
. , , . . TDD . , . , , . , . . . .
, TDD , : , , , . , , .
TDD — . , . , N , . , , , . N . , , , 1 god object 1 . , TDD : — .
, , — - . — . 1,5 .
. TDD
, , , . .