Adicionamos um novo tipo de teste de automação ao nosso framework de testes de automação existente. Esse novo tipo é conhecido como Spec. "Spec" é um termo para um teste criado seguindo a metodologia Behavior Driven Design (BDD) (desenvolvimento guiado por comportamento). Essa é uma metodologia muito comum usada em testes de desenvolvimento web, que adaptamos para o nosso framework C++.
Há vários motivos para começar a escrever Specs, inclusive elas:
- São autodocumentadas;
- São fluentes e geralmente DRY.
Dry (Don't Repeat Yourself, não repetível) ; - São muito mais fáceis de escrever código de teste ou código latente;
- Isolam expectativas (testes);
- Podem ser usadas para quase todos os tipos de testes (funcionais, de integração e unitários).
Como configurar um Spec
Existem dois métodos para definir o cabeçalho para se Spec, e ambos são semelhantes ao método existente que usamos para definir tipos de testes.
O método mais fácil é usar a macro DEFINE_SPEC , que usa exatamente os mesmos parâmetros que todo o resto das macros padrão de teste.
DEFINE_SPEC(MyCustomSpec, "MyGame.MyCustomSpec", EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask)
void MyCustomSpec::Define()
{
//@todo write my expectations here
}
A única outra alternativa é usar as macros BEGIN_DEFINE_SPEC e END_DEFINE_SPEC. Essas macros permitem que você defina seus próprios membros como parte do teste. Como você verá na próxima seção, é útil ter elementos relativas a esse ponteiro.
BEGIN_DEFINE_SPEC(MyCustomSpec, "MyGame.MyCustomSpec", EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask)
TSharedPtr<FMyAwesomeClass> AwesomeClass;
END_DEFINE_SPEC(MyCustomSpec)
void MyCustomSpec::Define()
{
//@todo write my expectations here
}
Só é preciso ter atenção porque devemos escrever a implementação para o membro Define() da sua classe de Spec, em vez do membro RunTests(), como faríamos para qualquer outro tipo de teste.
As Specs devem ser definidas em um arquivo com a extensão .spec.cpp e não ter a palavra "Test" (Teste) no nome. Por exemplo, a classe FItemCatalogService pode ter os arquivos ItemCatalogService.h, ItemCatalogService.cpp e ItemCatalogService.spec.cpp.
Esta é uma diretriz sugerida e não uma restrição técnica.
Como definir suas expectativas
Uma grande parte do BDD é que, em vez de testar uma implementação específica, você está testando expectativas de uma API pública. Isso torna seu teste muito menos falível e, portanto, mais fácil de manter, e mais provável de funcionar se várias implementações diferentes da mesma API surgirem.
Em uma Spec, você define suas expectativas por meio do uso de duas funções principais diferentes, Describe() e It().
Describe
Describe() é usada como uma forma de definir o escopo de expectativas complicadas, de forma que fiquem mais legíveis e não repetíveis. O uso de Describe() torna seu código menos repetível com base na interação que tem com outras funções de suporte, como BeforeEach() e AfterEach(), abordadas a seguir:
void Describe(const FString& Description, TFunction<void()> DoWork)
Describe() usa uma string que descreve o escopo das expectativas dentro dela e um lambda que define essas expectativas.
Você pode usar Describe() em cascata colocando um Describe() dentro de outro Describe().
Lembre-se de que Describe() não é um teste e não é executado durante uma execução de teste real. Elas são executadas apenas uma vez ao definir pela primeira vez as expectativas (ou testes) dentro da Spec.
It
It() é o trecho de código que define uma expectativa real para a Spec. Você pode chamar It() do método-raiz Define() ou de dentro de qualquer lambda Describe(). It() deve ser usada idealmente apenas para afirmar a expectativa, mas também pode ser usada para fazer a parte final da configuração do cenário sendo testado.
Geralmente, é uma melhor iniciar uma string de descrição de chamada It() com a palavra "should", o que implica "it should" (deve).
Como definir uma expectativa básica
Aqui está um exemplo de como reunir tudo para definir uma expectativa muito simples:
BEGIN_DEFINE_SPEC(MyCustomSpec, "MyGame.MyCustomClass", EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask)
TSharedPtr<FMyCustomClass> CustomClass;
END_DEFINE_SPEC(MyCustomSpec)
void MyCustomSpec::Define()
{
Describe("Execute()", [this]()
{
It("should return true when successful", [this]()
{
TestTrue("Execute", CustomClass->Execute());
});
It("should return false when unsuccessful", [this]()
{
TestFalse("Execute", CustomClass->Execute());
});
});
}
Como você pode ver, isso faz com que os testes sejam autodocumentados, especialmente se o programador dedicar um tempo para descrever a expectativa corretamente sem combinar expectativas divergentes. Pretende que a combinação de todas as Describe() e a chamada It() deva criar uma frase legível, por exemplo:
Execute() should return true when successful (Execute() deve retornar verdadeiro quando bem-sucedido)
Execute() should return false when unsuccessful (Execute() deve retornar falso quando malsucedido)
Veja a seguir um exemplo mais complicado da aparência de uma especificação madura atualmente na interface de usuário de teste de automação:
Neste exemplo, Driver, Element e Click são chamadas Describe(), com as várias mensagens "should..." sendo definidas pela chamada It().
Cada uma dessas chamadas It() torna-se um teste individual a ser executado e, portanto, pode ser executado isoladamente se uma falhar, enquanto as outras não. Isso facilita a manutenção dos testes, pois é menos complicado depurá-los. Também como os testes são autodocumentados e isolados, quando um falha, a pessoa que está lendo o relatório do teste tem uma compreensão muito mais específica sobre o que não está funcionando, em vez de apenas saber que um bucket muito grande chamado "Core" falhou. Assim, os problemas são direcionados para a pessoa certa mais rapidamente e é gasto menos tempo investigando os problemas.
Por fim, clicar em qualquer um dos testes acima levará diretamente para a instrução It() que a define.
Como uma expectativa de Spec se traduz em um teste
A seguir, apresentamos uma explicação detalhada; no entanto, entender o comportamento subjacente do tipo de teste Spec facilitará a compreensão de algumas das funcionalidades complexas a seguir.
O tipo de teste Spec executa a função-raiz Define() uma vez, mas não até que seja necessária. Durante essa execução, ele coleta todos os lambdas não Describe. Depois que Define() termina, ele voltará a passar por todas as lambdas ou blocos de código que coletou e gerará uma matriz de comandos latentes para cada It().
Portanto, cada bloco de código lambda BeforeEach(), It() e AfterEach() é reunido em uma cadeia de execução para um único teste. Quando solicitado a executar um teste específico, o tipo de teste Spec colocará todos os comandos na fila para esse teste específico para execução. Quando isso acontece, os blocos não continuam até que o bloco anterior tenha sinalizado o término de sua execução.
Funcionalidades adicionais
O topo de teste Spec oferece várias outras funcionalidades que facilitam a escrita de testes complicados. Mais especificamente, ele geralmente remove a necessidade de usar diretamente o sistema de comandos latentes do framework de testes de automação, que é poderoso e complicado.
Aqui está uma lista de funcionalidades aceitas pelo tipo de teste de Spec que podem ajudar em situações mais complicadas:
- BeforeEach e AfterEach
- AsyncExecution
- Latent Completion
- Parameterized Tests
- Redefine
- Disabling Tests
BeforeEach e AfterEach
BeforeEach() e AfterEach() são funções essenciais para escrever qualquer função além da especificação mais comum. BeforeEach() permite que você execute o código antes que o código It() subsequente seja executado. AfterEach() faz a mesma coisa, mas executará o código depois que o código It() for executado.
Lembre-se que cada "teste" é composto apenas por uma única chamada "It()".
Por exemplo:
BEGIN_DEFINE_SPEC(AutomationSpec, "System.Automation.Spec", EAutomationTestFlags::SmokeFilter | EAutomationTestFlags::ApplicationContextMask)
FString RunOrder;
END_DEFINE_SPEC(AutomationSpec)
void AutomationSpec::Define()
{
Describe("A spec using BeforeEach and AfterEach", [this]()
{
BeforeEach([this]()
{
RunOrder = TEXT("A");
});
It("will run code before each spec in the Describe and after each spec in the Describe", [this]()
{
TestEqual("RunOrder", RunOrder, TEXT("A"));
});
AfterEach([this]()
{
RunOrder += TEXT("Z");
TestEqual("RunOrder", RunOrder, TEXT("AZ"));
});
});
}
No nosso exemplo, o bloco de código é executado de cima para baixo, devido ao BeforeEach() ter sido definido, depois It() e AfterEach(). Embora não seja um requisito, sugerimos que você mantenha essa ordenação lógica das chamadas. Mas você pode misturar a ordem das três chamadas acima, e o resultado sempre produziria o mesmo teste.
Além disso, no exemplo acima, ele está verificando uma expectativa em AfterEach(), e isso é muito anormal e um efeito colateral de testar o próprio tipo de teste Spec. Como tal, não recomendamos o uso de AfterEach() para nada além da limpeza.
Você também pode criar várias chamadas BeforeEach() e AfterEach(), e elas serão chamadas na ordem em que são definidas. Assim como a primeira chamada BeforeEach() sendo executada antes da segunda chamada BeforeEach(), AfterEach() se comporta de maneira praticamente igual: com a primeira chamada executada antes da chamada subsequente.
BeforeEach([this]()
{
RunOrder = TEXT("A");
});
BeforeEach([this]()
{
RunOrder += TEXT("B");
});
It("will run code before each spec in the Describe and after each spec in the Describe", [this]()
{
TestEqual("RunOrder", RunOrder, TEXT("AB"));
});
AfterEach([this]()
{
RunOrder += TEXT("Y");
TestEqual("RunOrder", RunOrder, TEXT("ABY"));
});
AfterEach([this]()
{
RunOrder += TEXT("Z");
TestEqual("RunOrder", RunOrder, TEXT("ABYZ"));
});
Além disso, BeforeEach() e AfterEach() são afetados pelo escopo de Describe() em que são chamados. Ambos serão executados apenas para a chamada It() que esteja dentro do escopo no qual também são chamados.
Aqui está um exemplo complicado, com chamada ordenada incorretamente, que funciona corretamente.
BEGIN_DEFINE_SPEC(AutomationSpec, "System.Automation.Spec", EAutomationTestFlags::SmokeFilter | EAutomationTestFlags::ApplicationContextMask)
FString RunOrder;
END_DEFINE_SPEC(AutomationSpec)
void AutomationSpec::Define()
{
Describe("A spec using BeforeEach and AfterEach", [this]()
{
BeforeEach([this]()
{
RunOrder = TEXT("A");
});
AfterEach([this]()
{
RunOrder += TEXT("Z");
// Pode resultar em
// TestEqual("RunOrder", RunOrder, TEXT("ABCYZ"));
// ou este, com base em qual It() está sendo executado
// TestEqual("RunOrder", RunOrder, TEXT("ABCDXYZ"));
});
BeforeEach([this]()
{
RunOrder += TEXT("B");
});
Describe("while nested inside another Describe", [this]()
{
AfterEach([this]()
{
RunOrder += TEXT("Y");
});
It("will run all BeforeEach blocks and all AfterEach blocks", [this]()
{
TestEqual("RunOrder", RunOrder, TEXT("ABC"));
});
BeforeEach([this]()
{
RunOrder += TEXT("C");
});
Describe("while nested inside yet another Describe", [this]()
{
It("will run all BeforeEach blocks and all AfterEach blocks", [this]()
{
TestEqual("RunOrder", RunOrder, TEXT("ABCD"));
});
AfterEach([this]()
{
RunOrder += TEXT("X");
});
BeforeEach([this]()
{
RunOrder += TEXT("D");
});
});
});
});
}
AsyncExecution
O tipo de teste Spec também permite definir facilmente como um único bloco de código deve ser executado. Isso é feito simplesmente passando o tipo EAsyncExecution correto para a versão sobrecarregada de BeforeEach(), It() e/ou AfterEach().
Por exemplo:
BeforeEach(EAsyncExecution::TaskGraph, [this]()
{
// Configurar algumas coisas.
));
It("should do something awesome", EAsyncExecution::ThreadPool, [this]()
{
// Fazer algumas coisas.
});
AfterEach(EAsyncExecution::Thread, [this]()
{
// Destruir algumas coisas.
));
Cada um dos blocos de código acima será executado de maneira diferente, mas em uma ordem sequencial garantida. O bloco BeforeEach() será executado como uma tarefa em TaskGraph, It() será executada em um thread aberto no pool de threads, e AfterEach() gerará seu próprio thread dedicado apenas para executar um bloco de código.
Essas opções são extremamente úteis quando precisamos simular cenários sensíveis a threads, como com o Automation Driver (driver de automação).
A funcionalidade AsyncExecution pode ser combinada com a funcionalidade Latent Completion.
Conclusão Latente
Às vezes, você precisa escrever um teste que precisa realizar uma ação que use vários quadros, como ao realizar uma consulta. Nessas situações, você pode usar o membro sobrecarregado LatentBeforeEach(), LatentIt() e LatentAfterEach(). Todos esses membros são idênticos às variações não latentes, com a diferença que seus lambdas recebem um delegado simples chamado Done.
Ao usar as variações latentes, o tipo de teste Spec não continuará a execução para o próximo bloco de código na sequência de teste até que o bloco de código latente em execução ativamente invoque o delegado "Done".
LatentIt("should return available items", [this](const FDoneDelegate& Done)
{
BackendService->QueryItems(this, &FMyCustomSpec::HandleQueryItemComplete, Done);
});
void FMyCustomSpec::HandleQueryItemsComplete(const TArray<FItem>& Items, FDoneDelegate Done)
{
TestEqual("Items.Num() == 5", Items.Num(), 5);
Done.Execute();
}
Como é possível ver no exemplo, você pode passar o delegado Done como uma carga útil para outros retornos de chamada para torná-lo acessível ao código latente. Portanto, quando o teste acima for executado, ele não continuará a executar nenhum bloco de código AfterEach() para It() até que o delegado Done seja executado, mesmo que a execução do bloco de código It() já tenha sido concluída.
A funcionalidade Latent Completion pode ser combinada com a funcionalidade AsyncExecution.
Testes parametrizados
Às vezes, você precisa criar testes de uma maneira orientada por dados. E, às vezes, isso significa ler as entradas de um arquivo e gerar testes a partir dessas entradas. Outras vezes, pode ser simplesmente ideal para reduzir a duplicação de código. De qualquer forma, o tipo de teste Spec permite testes parametrizados de uma maneira muito natural.
Describe("Basic Math", [this]()
{
for (int32 Index = 0; Index < 5; Index++)
{
It(FString::Printf(TEXT("should resolve %d + %d = %d"), Index, 2, Index + 2), [this, Index]()
{
TestEqual(FString::Printf(TEXT("%d + %d = %d"), Index, 2, Index + 2), Index + 2, Index + 2);
});
}
});
Como você pode ver no exemplo acima, tudo o que você precisa fazer para criar testes parametrizados é chamar dinamicamente a outras funções Spec que estão passando os dados parametrizados como parte da carga útil de lambda enquanto gera uma descrição única.
Em alguns casos, o uso de testes parametrizados pode apenas criar um excesso de testes. Pode ser razoável simplesmente executar todos os cenários da entrada como parte de um único teste. Você deve considerar o número de entradas e os testes resultantes que são produzidos. O maior benefício de criar seus testes orientados por dados de uma maneira parametrizada é que cada teste pode ser executado isoladamente, facilitando a reprodução.
Redefinir
Ao trabalhar com testes parametrizados, às vezes pode ser conveniente, no tempo de execução, fazer uma alteração em um arquivo externo que controla a entrada e fazer com que os testes sejam atualizados automaticamente. Redefine() é um membro do tipo de teste Spec, que, quando chamado, executa novamente o processo Define(). Faz com que todo o bloco de código dos testes seja reunido novamente e comparado.
O método mais conveniente para fazer o mostrado acima seria criar um código que escute as alterações no arquivo de entrada e chame Redefine() no teste conforme necessário.
Como desabilitar testes
Cada membro Describe(), BeforeEach(), It() e AfterEach() do tipo de teste Spec tem uma variação com um "x" anterior. Por exemplo, xDescribe(), xBeforeEach(), xIt() e xAfterEach(). Essas variações são uma maneira mais simples de desabilitar um bloco de código ou Describe(). Se xDescribe() for usado, todo o código dentro de xDescribe() também será desabilitado.
Isso pode ser mais fácil do que comentar as expectativas que precisam de iteração.
Exemplos maduros
Você pode encontrar exemplos maduros do tipo de teste Spec em Engine/Plugins/Tests/AutomationDriverTests/Source/AutomationDriverTests/Private/AutomationDriver.spec.cpp. Atualmente, essa Spec inclui mais de cento e vinte expectativas e faz uso da maioria das funcionalidades avançadas em algum momento.
Nossa equipe do Inicializador também tem vários usos maduros da framework de Specs, e um dos usos mais maduros são as Specs escritas em torno de BuildPatchservices.