XCTest em execução

Teste

Mas, afinal, como garantir que mesmo após usar tantos conceitos o produto entregue é de fato bom?
Para responder essa pergunta, precisamos saber como é possível garantir qualidade na engenharia de software.

Uma forma comum e segura para garantir a qualidade do código é utilizar o recurso de testes automatizados.

O teste de software geralmente é a última etapa na construção de um programa, visando checar o seu nível de qualidade — exceto quando essa prática é o TDD, quando os testes são construídos antes da implementação final.

XCTest

Create and run unit tests, performance tests, and UI tests for your Xcode project. — Apple documentation.

XCTestCase

Anatomia de um teste em XCTest

O código de exemplo está dividido em seis partes e cada uma delas possui uma função dentro da execução de um teste.

1. Imports

2. Declaração

3. setUp

  • class setUp: Este setUp global tem como objetivo realizar as configurações que são comuns a todos os testes do seu respectivo TestCase. Para isso, durante o ciclo de vida de execução do TestCase ele será chamado uma vez antes de executar os testes.
  • setUp: Este setUp tem como objetivo realizar as configurações que devem ser reconfiguradas para cada teste. Para isso, durante o ciclo de vida de execução do TestCase ele será chamado uma vez antes de executar cada um dos testes.
  • setUpWithError: Este setUp tem como objetivo realizar as configurações que devem ser reconfiguradas para cada teste e que podem falhar, por isso ele pode lançar um erro. O erro lançado fará com que o TestCase falhe. Para isso, durante o ciclo de vida de execução do TestCase ele será chamado uma vez antes de executar cada um dos testes.

4. tearDown

  • class tearDown: Este tearDown global tem como objetivo desfazer as configurções feitas no setUp global. Para isso, durante o ciclo de vida de execução do TestCase ele será chamado uma vez depois de executar os testes.
  • tearDown: Este tearDown tem como objetivo desfazer as configurções feitas no seu respectivo setUp. Para isso, durante o ciclo de vida de execução do TestCase ele será chamado uma vez depois de executar cada um dos testes.
  • tearDownWithError: Este tearDown tem como objetivo desfazer as configurções feitas no respectivo setUp e que podem falhar, por isso ele pode lançar um erro. O erro lançado fará com que o TestCase falhe. Para isso, durante o ciclo de vida de execução do TestCase ele será chamado uma vez depois de executar cada um dos testes.

Super

No entanto, é importante lembrar que os métodos de setUp e tearDown implementados no XCTestCase são template methods, o que significa que eles não fazem nada. Ainda sim, é uma boa prática manter suas chamadas, pois caso escolha-se mudar para alguma outra implementação de TestCase — o framework de modo geral indica que a criação de base test cases é uma boa prática — não haverá comportamentos estranhos.

5. Test

6. Asserção

Executando

Mas, afinal, como o Xcode sabe quais testes executar e como executá-los?

Para entender melhor como o Xcode executa os testes é preciso entender o log resultante da execução. No playground a visualização é mais clara e objetiva.

No exemplo a seguir há um teste e ele será executado no playground:

O exemplo possui uma classe que será testada — sobre o nome de sut(system under test) — que possui dois métodos e cada um deles irá realizar um print.
O TestCase criado tem duas funções de teste, uma para cada método.

Para executar o TestCase é preciso acessar a propriedade defaultTestSuite — propriedade responsável por guardar as informações de todos os testes do seu respectivo TestCase — e nela utilizar o método run().

O log da execução deste playground é o seguinte:

Test Suite 'SomeTestCase' started at 2020-09-07 11:11:28.922
Test Case
'-[SomeTestCase testMetho1]' started.
>>>> METHOD 1 from <ClassTest: 0x600003d784d0>
Test Case
'-[SomeTestCase testMetho1]' passed (0.004 seconds).
Test Case
'-[SomeTestCase testMethod2]' started.
>>>> METHOD 2 from <ClassTest: 0x600003d78590>
Test Case
'-[SomeTestCase testMethod2]' passed (0.000 seconds).
Test Suite
'SomeTestCase' passed at 2020-09-07 11:11:28.929.
Executed 2 tests, with 0 failures (0 unexpected)
in 0.004 (0.007) seconds

Foram removidos os simbolos adicionados pelo lldb para melhor legibilidade.

O log demonstra alguns pontos importantes.
Primeiro, ao executar o TestCase, o framework lida com ele como uma Test Suite.
Segundo, cada teste é tratado como TestCase e, mesmo não realizando nenhuma asserção, os testes passarão. Isso demonstra uma das características do XCTest que pode gerar testes chamados falsos positivos, ou seja, aqueles que passam mas não validam nada.
Por fim, o log retorna o resultado da execução daquela suite (TestCase).

Evoluindo o teste

Executando o teste para garantir que nada tenha quebrado recebemos o seguinte log:

Test Suite 'SomeTestCase' started at 2020-09-07 11:47:23.508
Test Case '-[SomeTestCase testMetho1]' started.
>>>> METHOD 1 from <ClassTest: 0x6000030cc100>
Test Case '-[SomeTestCase testMetho1]' passed (0.004 seconds).
Test Case '-[SomeTestCase testMethod2]' started.
>>>> METHOD 2 from <ClassTest: 0x6000030cc140>
Test Case '-[SomeTestCase testMethod2]' passed (0.000 seconds).
Test Suite 'SomeTestCase' passed at 2020-09-07 11:47:23.515.
Executed 2 tests, with 0 failures (0 unexpected) in 0.005 (0.007) seconds

O log aponta que a refatoração do teste foi um sucesso, visto que nenhum teste quebrou. Porém, um detalhe é importante de se notar.
Ao analisar o print de cada teste é possivel perceber que os objetos são diferentes (0x6000030cc100 e 0x6000030cc140), mesmo trazendo o sut para o escopo do TestCase.

Este comportamento acontece pois o framework cria um instância nova do TestCase para cada teste.

Documentação da Apple. Citação: Para cada método de teste uma nova instância da classe de testes é alocada.
Documentação da Apple. Citação: Para cada método de teste uma nova instância da classe de testes é alocada.
Apple documentation

Por isso, ao analisar o log, cada teste é tratado como TestCase e o conjunto de instâncias criadas é o que foi chamado de Test Suite.

É possível encontrar essas instâncias através da propriedade tests da defaultTestSuite.

Execução completa

Fluxograma de execução XCTest

Ao solicitar a execução dos testes, o compilador irá procurar as classes de TestCase utilizando o Objective-C runtime e criará uma lista com as classes encontradas.
Cria-se então um XCTMain com essa lista.
O XCTMain irá iterar sobre a lista, acessando cada uma das suites e chamará o método run().

O método irá iterar sobre a propriedade tests (instâncias do Test Case) e executará o respectivo teste para cada uma das instâncias. Além disso, ao final da execução, incrementa-se o objeto com as informações da execução do teste (se foi um sucesso ou se houve falhas).

Algo semelhante ao que foi feito no playground, porém para cada um dos TestCase.

Indo além

Ao entender que esses objetos ficam em memória, deve-se lembrar então que por consequência, as respectivas propriedades também ficam alocadas.

É possivel perceber o comportamento no exemplo utilizado ao alterar a classe de testes para que execute um print também nos métodos init e deinit

Ao executar o playground novamente, percebe-se pelo log o comportamento descrito, uma vez que o deinit não é chamado:

>>>> INIT from <ClassTest: 0x6000016383f0>
>>>> INIT from <ClassTest: 0x600001638430>
Test Suite 'SomeTestCase' started at 2020-09-07 15:01:44.382
Test Case '-[SomeTestCase testMetho1]' started.
>>>> METHOD 1 from <ClassTest: 0x6000016383f0>
Test Case '-[SomeTestCase testMetho1]' passed (0.031 seconds).
Test Case '-[SomeTestCase testMethod2]' started.
>>>> METHOD 2 from <ClassTest: 0x600001638430>
Test Case '-[SomeTestCase testMethod2]' passed (0.000 seconds).
Test Suite 'SomeTestCase' passed at 2020-09-07 15:01:44.429.
Executed 2 tests, with 0 failures (0 unexpected) in 0.031 (0.047) seconds

Observação

Imagine um produto que possua por volta de 1000 (mil) arquivos de teste, sendo que cada arquivo possui no mínimo 6 testes. No final da execução teremos 6000 (seis mil) instâncias de teste em memória, cada uma com as respectivas propriedades criadas. Ao exercitar um pouco a imaginação, é possível imaginar que no final da execução, por volta de 9000 (nove mil) instâncias podem estar alocadas em memória.

Esse cenário descrito pode não causar problemas em Macs mais recentes e com mais poder computacional, porém, vale ressaltar que, em um projeto desse porte, nem todos os desenvolvedores terão máquinas que suportem tamanho consumo de memória.

Evoluindo um pouco mais esse cenário, chega-se a um cenário até mais comum. Em boa parte dos projetos, as CIs costumam ser Mac Minis — com configurações de harware medianas — que rodam até três pipelines em paralelo. Imagine como um Mac Mini se sairia com 9000 (nove mil) instâncias por pipeline, ou seja, 27000 (vinte e sete mil) instâncias.

Portando, logo percebe-se que os problemas de memória aumentam na mesma proporção de crescimento do projeto.

Solução

Automatic Reference Counting (ARC) é o gerenciador de uso de memória das aplicações no ecossistema Apple.

Para desalocar a instância em Swift deve-se atribuir o valor nil à propriedade.

Retomando o exemplo em que a inicialização estava na declaração podemos refatorá-lo para que seja possível liberar a memória. Para isso deve-se:

  1. Transformar a propriedade em uma variável opcional
  2. Criar e resetar a propriedade utilizando as funções de configuração e desconfiguração

Após a refatoração, chega-se a este resultado:

Executando o teste, o seguinte log é apresentado:

Test Suite 'SomeTestCase' started at 2020-09-07 15:04:24.404
Test Case '-[SomeTestCase testMetho1]' started.
>>>> INIT from <ClassTest: 0x60000027c1e0>
>>>> METHOD 1 from <ClassTest: 0x60000027c1e0>
>>>> DEINIT from <ClassTest: 0x60000027c1e0>
Test Case '-[SomeTestCase testMetho1]' passed (0.005 seconds).
Test Case '-[SomeTestCase testMethod2]' started.
>>>> INIT from <ClassTest: 0x60000027c1e0>
>>>> METHOD 2 from <ClassTest: 0x60000027c1e0>
>>>> DEINIT from <ClassTest: 0x60000027c1e0>
Test Case '-[SomeTestCase testMethod2]' passed (0.000 seconds).
Test Suite 'SomeTestCase' passed at 2020-09-07 15:04:24.411.
Executed 2 tests, with 0 failures (0 unexpected) in 0.005 (0.007) seconds

A partir do log acima, é possível notar que os objetos indesejados foram liberados da memória devido a utilização dos métodos setUp e tearDown.

É importante notar que para isso foi preciso transformar o sut em uma propriedade optional. Porém, como todo teste utilizará a propriedade sut, é pressuposto que a propriedade existirá durante o tempo de execução. Assim, utiliza-se o simbolo de force (!) para evitar lidar com um optional durante todo o teste.

Conclusão

Indo além

Agradecimentos

iOS Developer — Apple Developer Academy Alumni | Mackenzie. Studying Unit Tests.

iOS Developer — Apple Developer Academy Alumni | Mackenzie. Studying Unit Tests.