No
artigo anterior, nos familiarizamos com a pirâmide de testes e os benefícios dos testes automatizados. Mas a teoria geralmente é diferente da prática. Hoje, queremos falar sobre nossa experiência no teste de código de
aplicativo usado por milhões de usuários do iOS. E também sobre o caminho difícil que nossa equipe teve que seguir para obter um código estável.
A situação é a seguinte: suponha que os desenvolvedores tenham conseguido convencer a si mesmos e aos negócios da necessidade de cobrir a base de código com testes. Com o tempo, o projeto tornou-se mais de uma dúzia de milhares de unidades e mais de mil testes de interface do usuário. Uma base de teste tão grande deu origem a vários problemas, cuja solução queremos contar.
Na primeira parte do artigo, vamos nos familiarizar com as dificuldades que surgem ao trabalhar com testes de unidade limpos (sem integração); na segunda parte, consideraremos os testes de interface do usuário. Para descobrir como estamos melhorando a estabilidade dos testes, seja bem-vindo ao gato.
Em um mundo ideal, com código-fonte inalterado, os testes de unidade devem sempre mostrar o mesmo resultado, independentemente do número e sequência de partidas. E testes em queda constante não devem passar pela barreira do servidor de integração contínua (CI).
Na realidade, pode-se encontrar o fato de que o mesmo teste de unidade mostrará um resultado positivo ou negativo - o que significa "piscar". A razão para esse comportamento está na má implementação do código de teste. Além disso, esse teste pode passar no IC com uma execução bem-sucedida e, mais tarde, começará a recair sobre o Pull Request (PR) de outras pessoas. Em uma situação semelhante, há um desejo de desativar esse teste ou jogar roleta e executar o IC novamente. No entanto, essa abordagem é anti-produtiva, pois prejudica a credibilidade dos testes e carrega o IC com um trabalho sem sentido.
Esta edição foi destacada este ano na conferência internacional da Apple na WWDC:
- Esta sessão fala sobre testes paralelos, análise da cobertura de um código de destino individual com testes e também sobre a ordem de lançamento dos testes.
- Aqui, a Apple falou sobre o teste de solicitações de rede, hackers, notificações de testes e a velocidade dos testes.
Testes unitários
Para combater testes intermitentes, usamos a seguinte sequência de ações:

0. Avaliamos o código do teste de qualidade de acordo com critérios básicos: isolamento, correção de moka, etc. Seguimos a regra: com um teste intermitente, alteramos o código de teste, e não o código de teste.
Se este item não ajudar, faça o seguinte:
1. Fixamos e reproduzimos as condições sob as quais o teste se enquadra;
2. Encontre a razão pela qual a queda;
3. Altere o código de teste ou código de teste;
4. Vá para o primeiro passo e verifique se a causa da queda foi eliminada.
Play fall
A opção mais simples e mais óbvia é executar um teste de problema na mesma versão do iOS e no mesmo dispositivo. Como regra, neste caso, o teste é bem-sucedido e surge o pensamento: "Tudo funciona para mim localmente, vou reiniciar o assembly no CI". De fato, o problema não foi resolvido e o teste continua com outra pessoa.
Portanto, na próxima etapa de verificação, você precisa executar localmente todos os testes de unidade do aplicativo para identificar o impacto potencial de um teste em outro. Mas, mesmo após essa verificação, o resultado do seu teste pode ser positivo, mas o problema permanece sem ser detectado.
Se toda a sequência de teste foi bem-sucedida e a queda esperada não foi registrada, você pode repetir a execução um número significativo de vezes.
Para fazer isso, na linha de comando, você precisa executar um loop com xcodebuild:
#! /bin/sh x=0 while [ $x -le 100 ]; do xcodebuild -configuration Debug -scheme "TargetScheme" -workspace App.wcworkspace -sdk iphonesimulator -destination "platfrom=iOS Simulator, OS=11.3, name=iPhone 7" test >> "report.txt"; x=$(( $x +1 )); done
Como regra, isso é suficiente para reproduzir a queda e seguir para a próxima etapa - identificando a causa da queda registrada.
Razões para a queda e possíveis soluções
Considere as principais causas dos testes de unidade intermitentes que você pode encontrar em seu trabalho, as ferramentas para identificá-los e as possíveis soluções.
Existem três grupos principais de razões para a queda dos testes:
Má isolaçãoPor isolamento, queremos dizer um caso especial de encapsulamento, a saber: um mecanismo de linguagem que permite restringir o acesso de alguns componentes do programa a outros.
O isolamento do ambiente desempenha um papel importante, pois, para a pureza do teste, nada deve afetar as entidades testadas. Atenção especial deve ser dada aos testes que visam verificar o código. Eles usam entidades de estado global, como variáveis globais, Keychain, Rede, CoreData, Singleton, NSUserDefaults e assim por diante. É nessas áreas que surge o maior número de locais em potencial para a manifestação de mau isolamento. Suponha que, ao criar um ambiente de teste, seja definido um estado global, implicitamente usado em outro código de teste. Nesse caso, o teste que verifica o código sob teste pode começar a piscar - porque, dependendo da sequência de testes, duas situações podem surgir - quando o estado global está definido e quando não está definido. Freqüentemente, as dependências descritas estão implícitas; portanto, você pode esquecer acidentalmente de definir / redefinir esses estados globais.
Para tornar as dependências claramente visíveis, você pode usar o princípio de Injeção de Dependência (DI), a saber: passar a dependência pelos parâmetros do construtor ou pela propriedade do objeto. Isso facilitará a substituição de dependências simuladas em vez de um objeto real.
Assincronia de chamadaTodos os testes de unidade são realizados de forma síncrona. A dificuldade de testar a assincronia surge porque a chamada do método de teste no teste “congela” em antecipação à conclusão do escopo do teste de unidade. O resultado será uma queda estável no teste.
Para testar esse teste, há várias abordagens:
- Execute o NSRunLoop
- waitForExpectationsWithTimeout
Ambas as opções requerem que você especifique um argumento com um tempo limite. No entanto, não é possível garantir que o intervalo selecionado seja suficiente. Localmente, seu teste será aprovado, mas em um IC fortemente carregado, talvez não haja energia suficiente e ele cairá - a partir daqui um "piscar" aparecerá.
Vamos ter algum tipo de serviço de processamento de dados. Queremos verificar se, após receber uma resposta do servidor, ele transfere esses dados para processamento adicional.
Para enviar solicitações pela rede, o serviço usa o cliente para trabalhar com ele.
Esse teste pode ser gravado de forma assíncrona usando um servidor simulado para garantir respostas estáveis à rede.
@interface Service : NSObject @property (nonatomic, strong) id<APIClient> apiClient; @end @protocol APIClient <NSObject> - (void)getDataWithCompletion:(void (^)(id responseJSONData))completion; @end - (void)testRequestAsync {
Mas a versão síncrona do teste será mais estável e permitirá que você se livre do trabalho com tempos limite.
Para ele, precisamos de um APIClient simulado síncrono
@interface APIClientMock : NSObject <APIClient> @end @implementation - (void)getDataWithCompletion:(void (^)(id responseJSONData))completion { __auto_type fakeData = @{ @"key" : @"value" }; if (completion != nil) { completion(fakeData); } } @end
Então o teste parecerá mais simples e funcionará mais estável
- (void)testRequestSync {
A operação assíncrona pode ser isolada encapsulando uma entidade separada, que pode ser testada independentemente. O restante da lógica precisa ser testado de forma síncrona. Essa abordagem evitará a maioria das armadilhas trazidas pela assincronia.
Como opção, no caso de atualizar a camada da interface do usuário a partir do encadeamento em segundo plano, você pode verificar se estamos no encadeamento principal e o que acontecerá se fizermos uma chamada a partir do teste:
func performUIUpdate(using closure: @escaping () -> Void) {
Para uma explicação detalhada, consulte
o artigo de D. Sandell .
Teste de código fora do seu controleMuitas vezes esquecemos as seguintes coisas:
- a implementação dos métodos pode depender da localização do aplicativo,
- existem métodos particulares no SDK que podem ser chamados por classes de estrutura,
- a implementação dos métodos pode depender da versão do SDK

Os casos acima introduzem incerteza ao escrever e executar testes. Para evitar consequências negativas, você precisa executar testes em todos os locais e nas versões do iOS suportadas pelo seu aplicativo. Separadamente, deve-se notar que não há necessidade de testar o código cuja implementação está oculta.
Com isso, queremos concluir a primeira parte do artigo sobre testes automatizados do aplicativo iOS Sberbank Online, dedicado ao teste de unidade.
Na segunda parte do artigo, falaremos sobre os problemas que ocorreram ao escrever 1500 testes de interface do usuário, bem como receitas para superá-los.
O artigo foi escrito com
regno - Anton Vlasov, chefe de desenvolvimento e desenvolvedor do iOS.