Introdução ao teste de unidade no Unity

imagem

Você está curioso para saber como o teste de unidade funciona no Unity? Não sabe ao certo o que é o teste de unidade? Se você respondeu positivamente a essas perguntas, este tutorial será útil para você. Com isso, você aprenderá o seguinte sobre o teste de unidade:

  • O que é isso
  • Seu benefício
  • Vantagens e desvantagens
  • Como funciona no Unity usando o Test Runner
  • Como escrever e executar testes de unidade que serão testados

Nota : este tutorial pressupõe que você esteja familiarizado com a linguagem C # e os conceitos básicos de desenvolvimento no Unity. Se você é novo no Unity, verifique primeiro os outros tutoriais neste mecanismo .

O que é um teste de unidade?


Antes de se aprofundar no código, é importante entender claramente o que é o teste de unidade. Simplificando, o teste de unidade está testando ... unidades.

O teste de unidade (idealmente) foi projetado para testar uma unidade de código separada. A composição de uma "unidade" pode variar, mas é importante lembrar que o teste de unidade deve testar exatamente um "elemento" de cada vez.

Testes de unidade precisam ser criados para verificar se um pequeno trecho de código lógico em um cenário específico é executado exatamente como o esperado. Isso pode ser difícil de entender antes de você começar a escrever seus próprios testes de unidade, então vamos ver um exemplo:

Você escreveu um método que permite ao usuário inserir um nome. O método é escrito para que os números não sejam permitidos no nome e o próprio nome possa consistir em apenas dez ou menos caracteres. Seu método intercepta o pressionamento de tecla de cada tecla e adiciona o caractere correspondente ao campo de name :

 public string name = "" public void UpdateNameWithCharacter(char: character) { // 1 if (!Char.IsLetter(char)) { return; } // 2 if (name.Length > 10) { return; } // 3 name += character; } 

O que está acontecendo aqui:

  1. Se o caractere não for uma letra, o código sairá da função previamente e não o adicionará à string.
  2. Se o nome tiver dez ou mais caracteres, o código não permitirá que o usuário adicione outro caractere.
  3. Se essas duas verificações forem aprovadas, o código adicionará um caractere ao final do nome.

Esta unidade pode ser testada, porque é um "módulo" do trabalho realizado. Os testes de unidade aplicam a lógica do método.

Exemplo de teste de unidade


Como escrevemos testes de unidade para o método UpdateNameWithCharacter ?

Antes de começar a implementar esses testes de unidade, precisamos considerar cuidadosamente o que esses testes fazem e criar nomes para eles.

Dê uma olhada nos exemplos de nome de teste de unidade abaixo. Dos nomes, deve ficar claro que eles verificam:

UpdateNameDoesntAllowCharacterAddingToNameIfNameIsTenOrMoreCharactersInLength

UpdateNameAllowsLettersToBeAddedToName

UpdateNameDoesntAllowNonLettersToBeAddedToName

A partir desses nomes de métodos de teste, vemos que realmente verificamos se a "unidade" de trabalho é UpdateNameWithCharacter pelo método UpdateNameWithCharacter . Esses nomes de teste podem parecer muito longos e detalhados, mas são bons para nós.

Cada teste de unidade que você escreve faz parte de um conjunto de testes. O conjunto de testes contém todos os testes de unidade relacionados ao grupo lógico funcional (por exemplo, "testes de unidade de batalha"). Se algum teste do kit falhar, todo o conjunto de testes falhará.


Lançamento do jogo


Abra o projeto Crashteroids Starter (você pode baixá-lo aqui ) e abra a cena do jogo na pasta Assets / RW / Scenes .


Clique em Play para iniciar o Crashteroids e, em seguida, clique no botão Start Game . Mova a nave espacial com as setas esquerda e direita do teclado.

Para disparar um raio laser, pressione a barra de espaço . Se o feixe atingir o asteróide, a pontuação aumentará em um. Se um asteróide colidir com um navio, o navio explode e o jogo termina (com a capacidade de começar de novo).


Tente jogar um pouco e certifique-se de que após a colisão do asteróide com o navio apareça a inscrição Game Over.


Introdução ao Unity Test Runner


Agora que sabemos como o jogo funciona, é hora de escrever testes de unidade para verificar se tudo funciona como deveria. Portanto, se você (ou outra pessoa) decidir atualizar o jogo, você terá certeza de que a atualização não quebrará nada que funcionou antes.

Para escrever testes, você primeiro precisa aprender sobre o Unity Test Runner. O Test Runner permite executar testes e verificar se eles são aprovados com êxito. Para abrir o Unity Test Runner, selecione Janela ▸ Geral ▸ Test Runner .


Depois que o Test Runner abrir em uma nova janela, você poderá simplificar sua vida clicando na janela Test Runner e arrastando -a para o local próximo à janela Scene.


Preparando NUnit e pastas de teste


O Test Runner é um recurso de teste de unidade fornecido pelo Unity, mas usa a estrutura do NUnit . Quando você começar a trabalhar com testes de unidade com mais seriedade, recomendo estudar o wiki no NUnit para saber mais. Sobre tudo o que você precisa pela primeira vez será discutido neste artigo.

Para executar os testes, primeiro precisamos criar uma pasta de teste na qual as classes de teste serão armazenadas.

Na janela Projeto, selecione a pasta RW . Dê uma olhada na janela Test Runner e verifique se PlayMode está selecionado.

Clique no botão chamado Criar pasta de montagem de teste do PlayMode . Você verá uma nova pasta aparecer na pasta RW. Estamos satisfeitos com o nome padrão de testes , para que você possa simplesmente pressionar Enter .


Você pode estar se perguntando o que essas duas guias diferentes estão dentro do Test Runner.

A guia PlayMode é usada para testes realizados no modo Play (quando o jogo está sendo executado em tempo real). Os testes na guia EditMode são executados fora do modo Play, o que é conveniente para testar itens como comportamentos personalizados no Inspector.

Neste tutorial, abordaremos os testes do PlayMode. Mas quando você se sentir confortável, tente experimentar os testes no EditMode. Ao trabalhar com o Test Runner neste tutorial, sempre verifique se a guia PlayMode está selecionada .

O que há na suíte de testes?


Como aprendemos acima, um teste de unidade é uma função que testa o comportamento de um pequeno pedaço específico de código. Como o teste de unidade é um método, para executá-lo, ele deve estar no arquivo de classe.

O Test Runner ignora todos os arquivos de classe de teste e executa testes de unidade a partir deles. Um arquivo de classe contendo testes de unidade é chamado de suíte de testes.

No conjunto de testes, subdividimos logicamente nossos testes. Devemos separar o código de teste em conjuntos lógicos separados (por exemplo, um conjunto de testes para a física e um conjunto separado para a batalha). Neste tutorial, precisamos de apenas um conjunto de testes e é hora de criar um.

Preparando um Conjunto de Testes e um Conjunto de Testes


Selecione a pasta Testes e, na janela Test Runner , clique no botão Criar Script de Teste na pasta atual . Nome do novo arquivo TestSuite .


Além do novo arquivo C #, o mecanismo do Unity também cria outro arquivo chamado Tests.asmdef . Este é o arquivo de definição de montagem , usado para mostrar ao Unity onde estão as dependências do arquivo de teste. Isso é necessário porque o código final do aplicativo está contido separadamente do código de teste.

Se você tiver uma situação em que o Unity não possa encontrar arquivos ou testes de teste, verifique se há um arquivo de definição de montagem que inclua seu conjunto de testes. O próximo passo é configurá-lo.

Para que o código de teste tenha acesso às classes de jogo, criaremos uma montagem do código de classe e definiremos o link na montagem Testes. Clique na pasta Scripts para selecioná-la. Clique com o botão direito nessa pasta e selecione Criar Definition Definição de montagem .


Nomeie o arquivo GameAssembly .


Clique na pasta Testes e, em seguida, no arquivo de definição de criação Testes . No Inspetor, clique no botão de adição sob o cabeçalho Referências de definição de montagem .


Você verá o campo Referência ausente . Clique no ponto ao lado deste campo para abrir a janela de seleção. Selecione o arquivo GameAssembly .


Você deve ver o arquivo de montagem do GameAssembly na seção de links. Clique no botão Aplicar para salvar essas alterações.


Se você não seguir estas etapas, não poderá fazer referência aos arquivos de classe do jogo dentro dos arquivos de teste de unidade. Depois de lidar com isso, você pode prosseguir para o código.

Escrevemos o primeiro teste de unidade


Clique duas vezes no script TestSuite para abri-lo no editor de código. Substitua todo o código por este:

 using UnityEngine; using UnityEngine.TestTools; using NUnit.Framework; using System.Collections; public class TestSuite { } 

Quais testes precisamos escrever? Honestamente, mesmo em um jogo minúsculo como o Crashteroids, você pode escrever alguns testes para verificar se tudo está funcionando como deveria. Neste tutorial, nos restringimos a apenas áreas-chave: reconhecimento de colisão e mecânica básica de jogo.

Nota : quando se trata de escrever testes de unidade de um produto em um nível de produção, você deve dedicar tempo suficiente para levar em consideração todos os casos de fronteira que precisam ser testados em todas as áreas do código.

Como primeiro teste, é bom verificar se os asteróides estão realmente se movendo para baixo. Será difícil para eles colidirem com o navio se afastarem dele! Adicione o seguinte método e variável privada ao script TestSuite :

 private Game game; // 1 [UnityTest] public IEnumerator AsteroidsMoveDown() { // 2 GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game")); game = gameGameObject.GetComponent<Game>(); // 3 GameObject asteroid = game.GetSpawner().SpawnAsteroid(); // 4 float initialYPos = asteroid.transform.position.y; // 5 yield return new WaitForSeconds(0.1f); // 6 Assert.Less(asteroid.transform.position.y, initialYPos); // 7 Object.Destroy(game.gameObject); } 

Existem apenas algumas linhas de código, mas elas fazem muitas coisas. Então, vamos parar e lidar com cada parte:

  1. Este é um atributo . Os atributos definem comportamentos específicos do compilador. Este atributo informa ao compilador Unity que o código é um teste de unidade. Devido a isso, ele aparecerá no Test Runner ao iniciar os testes.
  2. Crie uma instância do jogo. Todo o resto está incorporado no jogo, portanto, quando o criarmos, ele conterá tudo o que precisa ser testado. Em um ambiente de produção, provavelmente todos os elementos não estarão dentro da mesma pré-fabricada. Portanto, você precisará recriar todos os objetos necessários na cena.
  3. Aqui criamos um asteróide para que possamos monitorar se ele está se movendo. O método SpawnAsteroid retorna uma instância do asteróide criado. O componente Asteroid possui um método Move (se você estiver curioso para saber como o movimento funciona, consulte o script Asteroid em RW / Scripts ).
  4. O rastreamento da posição inicial é necessário para garantir que o asteróide tenha descido.
  5. Todos os testes de unidade do Unity são corotinas, portanto, é necessário adicionar um retorno suave. Também adicionamos um intervalo de tempo de 0,1 segundos para simular a passagem de tempo pela qual o asteróide deveria descer. Se você não precisar simular uma etapa do tempo, poderá retornar nulo.
  6. Este é o estágio de afirmação , no qual afirmamos que a posição do asteróide é menor que a posição inicial (ou seja, ele se moveu para baixo). Compreender asserções é uma parte importante do teste de unidade, e o NUnit fornece vários métodos de asserção. Passar ou não passar no teste é determinado por esta linha.
  7. Obviamente, ninguém o repreenderá pela bagunça deixada após a conclusão dos testes, mas outros testes poderão falhar devido a isso. É sempre importante limpar (excluir ou redefinir) o código após o teste de unidade, para que, ao executar o próximo teste de unidade, não haja artefatos restantes que possam afetar esse teste. Basta que excluamos o objeto do jogo, pois para cada teste criamos uma instância completamente nova do jogo.

Passando nos testes


Bem, você escreveu seu primeiro teste de unidade, mas como você sabe se funciona? Claro, com o Test Runner! Na janela Test Runner, expanda todas as linhas com setas. Você deve ver o teste AsteroidsMoveDown na lista com círculos em cinza:


Um círculo cinza indica que o teste ainda não foi concluído. Se o teste foi iniciado e passou, uma seta verde é exibida ao lado. Se o teste falhar, um X vermelho será exibido próximo a ele. Execute o teste clicando no botão RunAll .


Isso criará uma cena temporária e executará o teste. Após a conclusão, você deve ver que o teste foi aprovado.


Você escreveu com sucesso seu primeiro teste de unidade, declarando que os asteróides criados estão se movendo para baixo.

Nota : antes de começar a escrever seus próprios testes de unidade, você precisa entender a implementação que está testando. Se você estiver curioso para saber como a lógica que está testando está funcionando, estude o código na pasta RW / Scripts .

Usando testes de integração


Antes de avançar mais fundo na toca do coelho dos testes de unidade, é hora de dizer o que são os testes de integração e como eles diferem dos testes de unidade.

Testes de integração são testes que verificam como os "módulos" do código funcionam juntos. "Módulo" é outro termo confuso. Uma diferença importante é que os testes de integração devem testar a operação do software na produção real (ou seja, quando um jogador realmente joga um jogo).


Digamos que você fez um jogo de batalha em que um jogador mata monstros. Você pode criar um teste de integração para garantir que, quando um jogador mate 100 inimigos, uma conquista seja aberta ("Realização").

Este teste afetará vários módulos de código. O mais provável é que ele se refira ao mecanismo físico (reconhecimento de colisão), despachantes inimigos (rastreando a saúde do inimigo e processando danos, bem como passando para outros eventos relacionados) e um rastreador de eventos que rastreia todos os eventos desencadeados (por exemplo, "o monstro está morto"). Então, quando for a hora de desbloquear a conquista, ele poderá ligar para o gerente de conquista.

O teste de integração simulará o jogador matando 100 monstros e verificará se a conquista está desbloqueada. É muito diferente do teste de unidade porque testa componentes de código grandes que trabalham juntos.

Neste tutorial, não estudaremos testes de integração, mas isso deve mostrar a diferença entre a unidade de trabalho (e por que está sendo testada) e o módulo do código (e por que está sendo testada).

Adicionando um teste a um conjunto de testes


O próximo teste testará o final do jogo quando o navio colidir com um asteróide. Com o TestSuite aberto no editor de código, adicione o teste mostrado abaixo no primeiro teste de unidade e salve o arquivo:

 [UnityTest] public IEnumerator GameOverOccursOnAsteroidCollision() { GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game")); Game game = gameGameObject.GetComponent<Game>(); GameObject asteroid = game.GetSpawner().SpawnAsteroid(); //1 asteroid.transform.position = game.GetShip().transform.position; //2 yield return new WaitForSeconds(0.1f); //3 Assert.True(game.isGameOver); Object.Destroy(game.gameObject); } 

Já vimos a maior parte desse código no teste anterior, mas existem algumas diferenças:

  1. Forçamos o asteróide e a nave a colidir, dando claramente ao asteróide a mesma posição que a nave. Isso criará uma colisão de seus hitboxes e levará ao final do jogo. Se você estiver curioso para saber como esse código funciona, consulte os arquivos Navio , Jogo e Asteróide na pasta Scripts.
  2. Uma etapa de tempo é necessária para que o evento Collision do mecanismo físico seja acionado, portanto, um atraso de 0,1 segundos é retornado.
  3. Essa afirmação é verdadeira e verifica se o sinalizador gameOver no script Game é verdadeiro. A bandeira é verdadeira durante a operação do jogo, quando a nave é destruída, ou seja, testamos para garantir que ela seja verdadeira após a destruição da nave.

Volte à janela Test Runner e você verá que um novo teste de unidade apareceu lá.


Desta vez, executaremos este em vez de todo o conjunto de testes. Clique em GameOverOccursOnAsteroidCollision e, em seguida, no botão Executar Selecionado .


E pronto, passamos em outro teste.


Estágios de sintonia e destruição


Você deve ter notado que em nossos dois testes há um código repetido: onde o objeto Game é criado e onde um link para o script Game é definido:

 GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game")); game = gameGameObject.GetComponent<Game>(); 

Você também notará que há uma repetição na destruição do objeto Jogo:

 Object.Destroy(game.gameObject); 

Ao testar isso acontece com muita frequência. Quando se trata de executar testes de unidade, na verdade existem duas fases: a fase Setup e a fase Tear Down .

Todo o código dentro do método Setup será executado antes do teste de unidade (neste conjunto) e todo código dentro do método Tear Down será executado após o teste de unidade (neste conjunto).

É hora de simplificar nossas vidas, movendo a configuração e o código de detalhamento para métodos especiais. Abra o editor de código e adicione o seguinte código no início do arquivo TestSuite , logo antes do primeiro atributo [UnityTest]:

 [SetUp] public void Setup() { GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game")); game = gameGameObject.GetComponent<Game>(); } 

O atributo SetUp indica que esse método é chamado antes da SetUp cada teste.

Em seguida, adicione o seguinte método e salve o arquivo:

 [TearDown] public void Teardown() { Object.Destroy(game.gameObject); } 

O atributo TearDown indica que esse método é chamado após a TearDown cada teste.

Depois de preparar o código de configuração e destruição, exclua as linhas de código presentes nesses métodos e substitua-as por chamadas aos métodos correspondentes. Depois disso, o código ficará assim:

 public class TestSuite { private Game game; [SetUp] public void Setup() { GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game")); game = gameGameObject.GetComponent<Game>(); } [TearDown] public void Teardown() { Object.Destroy(game.gameObject); } [UnityTest] public IEnumerator AsteroidsMoveDown() { GameObject asteroid = game.GetSpawner().SpawnAsteroid(); float initialYPos = asteroid.transform.position.y; yield return new WaitForSeconds(0.1f); Assert.Less(asteroid.transform.position.y, initialYPos); } [UnityTest] public IEnumerator GameOverOccursOnAsteroidCollision() { GameObject asteroid = game.GetSpawner().SpawnAsteroid(); asteroid.transform.position = game.GetShip().transform.position; yield return new WaitForSeconds(0.1f); Assert.True(game.isGameOver); } } 

Teste de game over e tiro a laser


Tendo preparado os métodos de ajuste e destruição que simplificam nossas vidas, podemos começar a adicionar novos testes nos quais eles são usados. O próximo teste é verificar se, quando um jogador clica em Novo jogo , o valor do gameOver bool não é verdadeiro. Adicione esse teste ao final do arquivo e salve-o:

 [UnityTest] public IEnumerator NewGameRestartsGame() { //1 game.isGameOver = true; game.NewGame(); //2 Assert.False(game.isGameOver); yield return null; } 

Isso já deve lhe parecer familiar, mas vale a pena mencionar o seguinte:

  1. Este trecho de código prepara esse teste para que o gameOver booleano gameOver seja verdadeiro. Ao chamar o método NewGame , ele deve novamente definir o sinalizador como false .
  2. Aqui argumentamos que o bool isGameOver é false , o que deve ser verdadeiro ao invocar um novo jogo.

Retorne ao Test Runner e você verá que há um novo teste NewGameRestartsGame . Execute este teste, como fizemos anteriormente, e você verá que ele é executado com êxito:


Declaração do raio laser


O próximo teste é adicionar o teste de que o raio laser disparado pelo navio está voando (semelhante ao primeiro teste de unidade que escrevemos). Abra o arquivo TestSuite no editor. Adicione o seguinte método e salve o arquivo:

 [UnityTest] public IEnumerator LaserMovesUp() { // 1 GameObject laser = game.GetShip().SpawnLaser(); // 2 float initialYPos = laser.transform.position.y; yield return new WaitForSeconds(0.1f); // 3 Assert.Greater(laser.transform.position.y, initialYPos); } 

Aqui está o que esse código faz:

  1. Obtém um link para o raio laser gerado emitido pelo navio.
  2. A posição inicial é registrada para que possamos verificar se está subindo.
  3. Essa afirmação é consistente com a afirmação do teste de unidade AsteroidsMoveDown , mas agora afirmamos que o valor é maior (ou seja, o laser sobe).

Test Runner. LaserMovesUp :


, , .

,


, . TestSuite , :

 [UnityTest] public IEnumerator LaserDestroysAsteroid() { // 1 GameObject asteroid = game.GetSpawner().SpawnAsteroid(); asteroid.transform.position = Vector3.zero; GameObject laser = game.GetShip().SpawnLaser(); laser.transform.position = Vector3.zero; yield return new WaitForSeconds(0.1f); // 2 UnityEngine.Assertions.Assert.IsNull(asteroid); } 

:

  1. , .
  2. . , UnityEngine.Assertions ? , Unity Null , «» Null. NUnit Assert.IsNull() Unity null. null Unity, UnityEngine.Assertions.Assert, Assert NUnit.

Test Runner . .



- — , . . , (Test Driven Development, TDD).

TDD, . , , , , . , , .

, . -, , .

: — , . , . «» , , . , . , . , , .

, - :

-


- , :

  • , , .
  • , (- ).
  • .
  • .
  • ( ).

-


-. :

  • , .
  • .
  • .
  • , .
  • , -.
  • ( ), .
  • - .
  • UI .
  • .
  • .

,


. , TestSuite :

 [UnityTest] public IEnumerator DestroyedAsteroidRaisesScore() { // 1 GameObject asteroid = game.GetSpawner().SpawnAsteroid(); asteroid.transform.position = Vector3.zero; GameObject laser = game.GetShip().SpawnLaser(); laser.transform.position = Vector3.zero; yield return new WaitForSeconds(0.1f); // 2 Assert.AreEqual(game.score, 1); } 

, , , . :

  1. , . , .
  2. , game.score 1 ( 0, ).

Test Runner, , :


Awesome! .

Para onde ir a seguir?


. , , .

, - Unity. , -, , -.

? . - . :

  • .
  • .
  • .

-, mock- . .

NUnit , NUnit.

.

!

Source: https://habr.com/ru/post/pt456090/


All Articles