Será que está tudo conectado?

Usando Mirror para testar referências em Swift

Giovani Pereira
iFood Tech
6 min readJan 16, 2020

--

Já teve algum bug que a solução era apenas setar um delegate? Ou uma feature que quebrou porque uma referência não estava configurada corretamente? Essas coisas acontecem no dia-a-dia de todo mundo, mas poderiam estar protegidas se tivessem algum teste unitário que cobrisse suas referências. Mas… como fazer isso?

Se você trabalha com Swift em projetos maiores, provavelmente estamos falando que seu app está escrito em arquiteturas um pouco mais complexas que MVC (Model View Controller) - e essa arquitetura pode ser basicamente qualquer sopa de letrinhas como MVVM, VIP, VIPER, MVMC, etc.

Arquiteturas mais complexas estão sujeitas a bugs mais complexos. Aqui no iFood usamos VIP, que é uma arquitetura baseada no Clean Swift. Já tivemos bugs que foram resolvidos simplesmente ajustando uma referência que estava faltando ou injetando uma dependência corretamente. Esses problemas podem ser difíceis de encontrar, porque observando o código, o fluxo parece estar correto, mas na verdade o que faltou foi configurar sua cena corretamente.

Esquema de uma arquitetura VIP.
Esquema de uma cena em VIP. As linhas tracejadas representam ligações fracas entre os componentes.

A nossa View é a camada que representa a nossa UIViewController em conjunto com a UIView apresentada, ou seja, a camada que exibe e recebe interações com o usuário. A partir daí os comandos trafegam para o Interactor que contém as lógicas de negócio, depois para o presenter com a lógica de apresentação e, por fim, é apresentado novamente na camada da view.

A referência do Presenter para a ViewController é fraca, para evitar um retain-cycle, bem como a referência do Router para a ViewController.

Com essa arquitetura montada, sempre que quisermos criar uma cena nova, teremos que considerar que existe um objeto responsável por instanciá-la, que chamaremos de Creator.

Um simples Creator

A responsabilidade do nosso creator é de concentrar toda a inicialização da nossa cena e garantir que tudo está conectado corretamente. Vamos ver uma situação onde criamos uma cena simples:

Poderíamos escrever um teste para validar se o retorno do nosso Creator é do tipo que esperamos, uma ViewController. Seria um teste válido, mas não garantiria que todos os outros componentes da cena estão ligados entre si corretamente.

Encontrando propriedades com Reflection

Basicamente queremos saber se nosso ViewController tem uma referência para o Interactor, e que esse Interactor é do tipo esperado (SceneAInteractorao invés de SceneBInteractor). Como tudo é conectado por protocolos para aumentar a testabilidade, poderia haver mais de uma implementação possível, mas apenas uma correta para esse cenário. E o Interactor deve ter uma referência para o Presenter… e assim por diante, até que todas as referências da nossa arquitetura estejam cobertas.

O primeiro problema é que não temos uma referência do Interactor ou do Presenter quando instanciamos a cena, apenas do viewController, que é a estrutura retornada pela função CreateScene. E mais, algumas dessas propriedades que queremos validar se estão corretas, são privadas.

É aí que o Mirror entra em ação.

O Mirror, na verdade, representa uma feature comum de linguagens de programação chamada Reflection, que permite inspecionar e trabalhar sobre os conteúdos que compõem um objeto em runtime. No caso do Swift, é uma struct que reflete um objeto e traz informações da sua composição, seja de suas propriedades, tuplas, enums, etc. Por exemplo:

O Mirror tem uma propriedade chamada children que é uma coleção de tuplas (label: Optional<String>, value: Any) que podemos iterar sobre ela, onde o label é o nome da propriedade e ovalue do seu respectivo valor. Se observarmos o exemplo em código acima, quando acessamos o primeiro item dessa coleção podemos ver que seu label é um "x" e seu valor Int(21) , então podemos usar essas informações para iterar sobre os children , tanto pelo label quanto pelo tipo do seu value, e encontrar a propriedade que estamos procurando.

A graça do Mirror é que ele não se preocupa com a visibilidade dos itens, então propriedades privadas também estarão refletidas e podem ser encontradas na struct.

Reflection tem várias aplicações diferentes, mas aqui vamos focar em como usá-la para fazer testes de referências entre objetos.

Vale ressaltar que o children vai conter apenas as propriedades armazenadas pelo objeto refletido. Propriedades computadas ou funções não estarão descritas ali dentro 😉!

Encontrando referências

Com o auxílio do Mirror, podemos agora inspecionar propriedades privadas e validar seus valores e/ou referências no projeto - basta criar um Mirror de um objeto, acessar seus filhos (children) e pegar a referência que queremos.

Nesse caso, a constante interactor passa a ser uma referência para o próprio interactor do viewController instanciado, em que podemos acessar todas as suas propriedades, chamar seus métodos e até mesmo fazer um Mirror que o reflete.

Como cada child tem um label, que é o nome dessa propriedade na classe, e um value que é o próprio valor dessa propriedade, encontrando o value correto, podemos averiguar seu valor ou para qual objeto ele está apontando.

Note que para fazer isso, foi preciso saber o nome da propriedade que queríamos extrair, iterando sobre os filhos do Mirror e seu tipo para fazer cast, pois o value é uma propriedade do tipo Any.

Poderíamos ter feito a mesma coisa procurando o primeiro filho que tivesse o value do tipo esperado:

Mas isso seria um problema caso o Mirror tivesse mais de uma propriedade com o mesmo tipo!

Testando nossas referências

Agora que sabemos extrair as propriedades, podemos testar se está tudo conectado da forma que esperamos.

Por exemplo, na nossa arquitetura de VIP, o Presenter tem uma referência fraca para o ViewController que é setada no nosso Creator, e precisamos garantir que essa referência esteja setada corretamente. Com isso, se por algum motivo alguém apagar a linha que faz essa referência, não há nenhum erro de compilação, mas nosso teste vai quebrar e o desenvolvedor será avisado desse problema.

Pra isso podemos extrair com o Mirror o interactor da cena e dele o presenter, para depois testar se ele está conectado corretamente.

Pronto! Criamos um teste que verifica se nosso configurator está colocando a referência corretamente no nosso presenter.

O operador === compara se os dois operandos apontam para o mesmo objeto.

O guard ainda garante que presentersView não é opcional. Se for, ele falha o teste, e o assert garante que o Presenter tem uma referência para o viewController que é o mesmo gerado pelo creator — como diz nossa arquitetura.

Aqui colocamos os Mirror dentro de um guard let, porque o resultado dessa operação é opcional, e fazer um Mirror de um objeto opcional na verdade é fazer reflection de um enum com cases .some(Value) e .none. Iterar sobre os children desse cara não é a mesma coisa que iterar sobre os de um objeto não opcional, e não é o que estamos buscando nesse caso.

Caso você queira encontrar alguma propriedade que não seja por referência, mas sim por valor, como structs ou enums, não faz sentido comparar usando o operador ===, pois elas apenas guardam valores, mas você poderia testar se os valores guardados estão corretos.

Deixando tudo um pouco mais bonito

O Mirror dá vários super poderes para a gente no Swift, mas a sintaxe acaba ficando uma coisa meio feia quando precisamos fazer mais de um Mirror em sequência para extrair propriedades encadeadas.

Particularmente, eu também não gosto das strings com o nome das propriedades ao longo do código de teste, nem de ficar explicitamente criando os Mirrors. Então, uma solução legal é abstrair todo esse processo do reflection em uma função e criar constantes para o nome das propriedades:

Assim, nosso teste do Creator fica muito mais legível:

E com isso você pode fazer testes bonitos pra todas as suas referências na arquitetura e garantir que seu Creator está ligando tudo corretamente, que suas dependências foram bem injetadas, que aquele parâmetro que você passou na criação do módulo chegou na classe esperada, que todos os delegates e data sources estão setados corretamente… E assim, previnir toda uma gama de bugs no seu projeto.

Quando começamos a testar nossos Creators usando reflection aqui no iFood, prevenimos vários bugs, principalmente porque enquanto criamos os testes, é possível identificar esses problemas e ir corrigindo referências ou valores injetados errados nas cenas antes de finalizar o código.

E agora que você aprendeu um pouco mais de reflection nos testes, vai poder prevenir vários desses bugs também :)
Bons testes! | ¡sǝʇsǝʇ suoꓭ

Quer receber conteúdos exclusivos criados pelos nossos times de tecnologia? Inscreva-se.

--

--