Olá novamente! Falta menos de uma semana para o início das aulas do grupo no curso
"Desenvolvedor C ++" . Nesse sentido, continuamos a compartilhar material útil traduzido especificamente para os alunos deste curso.

O teste de unidade do seu código com modelos lembra-se de vez em quando. (Você está testando seus modelos, certo?) Alguns modelos são fáceis de testar. Alguns não são. Às vezes, há uma falta de clareza final em relação à implementação de mock-code (stub) no modelo testado. Eu observei várias razões pelas quais a incorporação de código se torna complicada.
Abaixo, dei alguns exemplos com aproximadamente crescente complexidade da implementação de código.
- O modelo usa um argumento de tipo e um objeto do mesmo tipo por referência no construtor.
- O modelo usa um argumento de tipo. Faz uma cópia do argumento do construtor ou simplesmente não o aceita.
- Um modelo usa um argumento de tipo e cria vários modelos interconectados sem funções virtuais.
Vamos começar com um simples.
O modelo usa um argumento de tipo e um objeto do mesmo tipo por referência no construtor
Este caso parece simples, porque o teste de unidade simplesmente cria uma instância do modelo de teste com o tipo de stub. Alguma declaração pode ser verificada para a classe simulada. E isso é tudo.
Naturalmente, o teste com apenas um argumento de tipo não diz nada sobre o restante do número infinito de tipos que podem ser passados para o modelo. Uma maneira elegante de dizer a mesma coisa: os padrões são conectados por um quantificador de generalidade; portanto, talvez tenhamos que nos tornar um pouco mais perspicazes para testes mais científicos. Mais sobre isso mais tarde.
Por exemplo:
template <class T> class TemplateUnderTest { T *t_; public: TemplateUnderTest(T *t) : t_(t) {} void SomeMethod() { t->DoSomething(); t->DoSomeOtherThing(); } }; struct MockT { void DoSomething() {
O modelo usa um argumento de tipo. Faz uma cópia do argumento do construtor ou simplesmente não o aceita
Nesse caso, o acesso ao objeto dentro do modelo pode não ser possível devido aos direitos de acesso. Você pode usar classes de
friend
.
template <class T> class TemplateUnderTest { T t_; friend class UnitTest; public: void SomeMethod() { t.DoSomething(); t.DoSomeOtherThing(); } }; class UnitTest { void Test2() { TemplateUnderTest<MockT> test; test.SomeMethod(); assert(DoSomethingWasCalled(test.t_));
UnitTest :: Test2
tem acesso ao corpo de TemplateUnderTest e pode verificar instruções na cópia interna do MockT.
Um modelo usa um argumento de tipo e cria vários modelos interconectados sem funções virtuais
Para este caso, examinarei um exemplo do mundo real:
RPC assíncrono do Google .
No C ++, o assíncrono gRPC possui algo chamado CallData, que, como o nome indica, armazena
dados relacionados a uma chamada RPC . O modelo CallData pode manipular vários tipos diferentes de RPCs. Portanto, é natural que seja implementado precisamente pelo modelo.
Um CallData genérico aceita dois argumentos de tipo: Solicitação e Resposta. Pode ficar assim:
template <class Request, class Response> class CallData { grpc::ServerCompletionQueue *cq_; grpc::ServerContext context_; grpc::ServerAsyncResponseWriter<Response> responder_;
O teste de unidade para o modelo CallData deve verificar o comportamento de HandleRequest e HandleResponse. Essas funções invocam várias funções de membro. Portanto, verificar a integridade da chamada é fundamental para a integridade do CallData. No entanto, existem truques.
- Alguns tipos do namespace grpc são criados internamente e não são passados pelo construtor.
ServerAsyncResponseWriter
e ServerContext
, por exemplo. grpc :: ServerCompletionQueue
é passado ao construtor como argumento, mas não possui funções virtuais. Apenas um destruidor virtual.grpc :: ServerContext
é criado internamente e não possui funções virtuais.
A questão é como testar o CallData sem usar o gRPC completo nos testes? Como simular ServerCompletionQueue? Como simular ServerAsyncResponseWriter, que por si só é um modelo? e assim por diante ...
Sem funções virtuais, substituir o comportamento do usuário se torna uma tarefa complexa. Tipos codificados permanentemente, como grpc :: ServerAsyncResponseWriter, não podem ser modelados porque são, hmm, codificados permanentemente e não implementados.
Há pouco sentido em transmiti-los como argumentos construtores. Mesmo se você fizer isso, pode não fazer sentido, pois podem ser classes finais ou simplesmente não ter funções virtuais.
Então o que fazemos?
Solução: Traços

Em vez de incorporar o comportamento personalizado herdando de um tipo genérico (como é feito na programação orientada a objetos), INSIRA O TIPO. Nós usamos traços para isso. Somos especializados em características de diferentes maneiras, dependendo do tipo de código: um código de produção ou um código de teste de unidade.
Considere
CallDataTraits
template <class CallData> class CallDataTraits { using ServerCompletionQueue = grpc::ServerCompletionQueue; using ServerContext = grpc::ServerContext; using ServerAsyncResponseWriter = grpc::ServerAsyncResponseWrite<typename CallData::ResponseType>; };
Este é o modelo principal para a característica usada para o código de produção. Vamos usá-lo em um CallDatatemplate.
Observando o código acima, fica claro que o código do aplicativo ainda usa tipos do namespace grpc. No entanto, podemos substituir facilmente os tipos grpc por tipos fictícios. Veja abaixo.
As características nos permitiram escolher os tipos implementados no CallData, dependendo da situação. Este método não requer desempenho adicional, pois não foram criadas funções virtuais desnecessárias para adicionar funcionalidade. Esta técnica também pode ser usada nas aulas finais.
Como você gosta do material? Escreva comentários. E até a porta aberta ;-)