Teste de unidade na arquitetura Clean Swift

Olá leitor!


Não é segredo que o teste é parte integrante de qualquer desenvolvimento. Nos artigos anteriores, abordamos a arquitetura básica da arquitetura Clean Swift e agora é hora de aprender como cobrir sua unidade com testes. Tomaremos como base o projeto do artigo sobre Trabalhadores e analisaremos os principais pontos.



Teoria


Graças à injeção de dependência e orientada a protocolos , todos os componentes de cena no Clean Swift são independentes entre si e podem ser testados separadamente. Como exemplo, o Interactor depende do Presenter e Worker , mas essas dependências são opcionais e baseadas em protocolo. Assim, o Interactor pode executar seu trabalho (embora inferior) sem o Presenter e Worker , e também podemos substituí-los por outros objetos assinados sob seus protocolos.


Como queremos testar cada componente separadamente, precisamos substituir as dependências por pseudo componentes . Isso nos ajudará espiões (Spy). Spy são objetos de teste que implementam os protocolos que queremos injetar e rastrear as chamadas de método neles. Em outras palavras, criamos o Spy for Presenter e Worker e os injetamos no Interactor para rastrear chamadas de método.



Para ser sincero, acrescentarei que também existem objetos de teste ( duplas de teste ) Dummy , Fake , Stub e Mock . Mas, dentro da estrutura deste artigo, não os afetamos. Leia mais aqui - TestDoubles


Prática


Terminado com as palavras, vamos ao que interessa. Para economizar seu tempo, consideraremos o código abstrato sem entrar em detalhes da implementação de cada método. Detalhes do código do aplicativo podem ser encontrados aqui: CleanSwiftTests


Para cada componente da cena, criamos um arquivo com testes e testes duplos para dependências de componentes ( Test Doubles ).


Um exemplo dessa estrutura:



A estrutura de cada arquivo de teste (para componentes) tem a mesma aparência e segue aproximadamente a seguinte sequência de gravação:


  • Declaramos uma variável com SUT (o objeto que vamos testar) e variáveis ​​com suas principais dependências
  • Inicializamos o SUT e suas dependências em setUp () e os limpamos em tearDown ()
  • Métodos de teste

Como discutimos na teoria, cada componente da cena pode ser testado separadamente. Podemos injetar duplicatas de teste ( Spy ) em suas dependências e, assim, monitorar a operação dos métodos de nosso SUT . Vamos analisar mais de perto o processo de escrever testes usando o exemplo do Interator da cena inicial .


O HomeInteractor depende de dois objetos - Presenter e Worker . Ambas as variáveis ​​na classe têm um tipo de protocolo. Isso significa que podemos criar duplicatas de teste assinadas sob os protocolos HomePresentationLogic e HomeWorkingLogic e injetá-las no HomeInteractor .


final class HomeInteractor: HomeBusinessLogic, HomeDataStore { // MARK: - Public Properties var presenter: HomePresentationLogic? lazy var worker: HomeWorkingLogic = HomeWorker() var users: [User] = [] var selectedUser: User? // MARK: - HomeBusinessLogic func fetchUsers(_ request: HomeModels.FetchUsers.Request) { // ... } func selectUser(_ request: HomeModels.SelectUser.Request) { // ... } 

Vamos testar dois métodos:


  • fetchUsers (:) . Responsável por obter uma lista de usuários pela API . Uma solicitação de API é enviada usando Worker .
  • selectUser (:) . Responsável por selecionar o usuário ativo ( selectedUser ) na lista de usuários carregados ( usuários ).

Para começar a escrever os testes do Interactor , precisamos criar espiões que rastrearão a invocação de métodos em HomePresentationLogic e HomeWorkingLogic . Para fazer isso, crie a classe HomePresentationLogicSpy no diretório 'CleanSwiftTestsTests / Stores / Home / TestDoubles / Spies', assine o protocolo HomePresentationLogic e implemente o método deste protocolo.


 final class HomePresentationLogicSpy: HomePresentationLogic { // MARK: - Public Properties private(set) var isCalledPresentFetchedUsers = false // MARK: - Public Methods func presentFetchedUsers(_ response: HomeModels.FetchUsers.Response) { isCalledPresentFetchedUsers = true } } 

Tudo é extremamente transparente aqui. Se o método presentFetchedUsers ( HomePresentationLogic ) for chamado, definiremos o valor da variável isCalledPresentFetchedUsers como true . Assim, podemos rastrear se esse método foi chamado durante o teste do Interactor .


Usando o mesmo princípio, crie HomeWorkingLogicSpy . Uma diferença, chamamos de conclusão , porque parte do código no Interactor será agrupada no fechamento desse método. Os métodos HomeWorkingLogic lidam com solicitações de rede. Precisamos evitar solicitações reais de rede durante o teste. Para fazer isso, substituí-lo por um teste, que rastreia chamadas de método e retorna dados de modelo, mas não faz nenhuma solicitação à rede.


 final class HomeWorkingLogicSpy: HomeWorkingLogic { // MARK: - Public Properties private(set) var isCalledFetchUsers = false let users: [User] = [ User(id: 1, name: "Ivan", username: "ivan91"), User(id: 2, name: "Igor", username: "igor_test") ] // MARK: - Public Methods func fetchUsers(_ completion: @escaping ([User]?) -> Void) { isCalledFetchUsers = true completion(users) } } 

Em seguida, criamos a classe HomeInteractorTests , com a qual testaremos o HomeInteractor .


 final class HomeInteractorTests: XCTestCase { // MARK: - Private Properties private var sut: HomeInteractor! private var worker: HomeWorkingLogicSpy! private var presenter: HomePresentationLogicSpy! // MARK: - Lifecycle override func setUp() { super.setUp() let homeInteractor = HomeInteractor() let homeWorker = HomeWorkingLogicSpy() let homePresenter = HomePresentationLogicSpy() homeInteractor.worker = homeWorker homeInteractor.presenter = homePresenter sut = homeInteractor worker = homeWorker presenter = homePresenter } override func tearDown() { sut = nil worker = nil presenter = nil super.tearDown() } // MARK: - Public Methods func testFetchUsers() { // ... } func testSelectUser() { // ... } } 

Indicamos três variáveis ​​principais - sut , worker e apresentador .


Em setUp (), inicialize os objetos necessários, injete dependências no Interactor e atribua objetos a variáveis ​​de classe.
Em tearDown (), limpamos variáveis ​​de classe para a pureza do experimento.


O método setUp () é chamado antes do início do método de teste, por exemplo testFetchUsers () e tearDown () quando este método conclui seu trabalho. Assim, recriamos o objeto de teste ( sut ) antes de cada método de teste ser executado.


A seguir, os próprios métodos de teste. A estrutura é dividida em 3 blocos lógicos principais - a criação dos objetos necessários, o lançamento do método testado no SUT e a verificação dos resultados. No exemplo abaixo, criamos uma solicitação (no nosso caso, não possui parâmetros), executamos o método fetchUsers (:) Interactor'a e, em seguida, verificamos se os métodos necessários foram chamados em HomeWorkingLogicSpy e HomePresentationLogicSpy . Também verificamos se o Interactor salvou os dados de teste recebidos do Worker em seu DataStore .


 func testFetchUsers() { let request = HomeModels.FetchUsers.Request() sut.fetchUsers(request) XCTAssertTrue(worker.isCalledFetchUsers, "Not started worker.fetchUsers(:)") XCTAssertTrue(presenter.isCalledPresentFetchedUsers, "Not started presenter.presentFetchedUsers(:)") XCTAssertEqual(sut.users.count, worker.users.count) } 

Testaremos a escolha do usuário por uma estrutura semelhante. Declaramos as variáveis expectationId e expectationName , pelas quais compararemos o resultado da seleção do usuário. A variável users armazena uma lista de teste de usuários que atribuímos ao Interactor . Porque Os métodos de teste são chamados independentemente um do outro, e em tearDown () zeramos os dados, a lista de usuários do Interactor fica vazia e precisamos preenchê-los com algo. E então verificamos se o usuário foi designado no DataStore Interactor'a , depois de chamar sut.selectUser (:) , e se o usuário é o correto.


 func testSelectUser() { let expectationId = 2 let expectationName = "Vasya" let users = [ User(id: 1, name: "Ivan", username: "ivan"), User(id: 2, name: "Vasya", username: "vasya91"), User(id: 3, name: "Maria", username: "maria_love") ] let request = HomeModels.SelectUser.Request(index: 1) sut.users = users sut.selectUser(request) XCTAssertNotNil(sut.selectedUser, "User not selected") XCTAssertEqual(sut.selectedUser?.id, expectationId) XCTAssertEqual(sut.selectedUser?.name, expectationName) } 

Testar Presenter'a e ViewController'a ocorre no mesmo princípio, com diferenças mínimas. Uma das diferenças é que, para testar o ViewController, você precisará criar uma UIWindow e obter o controlador do Storyboard em setUp () , além de criar objetos Spy em tabelas e coleções. Mas essas nuances variam de acordo com as necessidades.


Para garantir a integridade, recomendo que você se familiarize com o projeto pelo link no final do artigo.


Conclusão


Abordamos os princípios básicos de teste de aplicativos na arquitetura Clean Swift . Ele não possui diferenças fundamentalmente fortes em relação a projetos de teste em outras arquiteturas, todos os mesmos testes, injeção e protocolos. O principal é não esquecer que cada ciclo VIP deve ter uma (e apenas uma!) Responsabilidade. Isso tornará o código mais limpo e os testes mais óbvios.


Link para o projeto: CleanSwiftTests
Ajuda para escrever um artigo: Bastien


Série de artigos


  1. Visão geral da arquitetura Clean Swift
  2. Roteador e transmissão de dados na arquitetura Clean Swift
  3. Trabalhadores na arquitetura Clean Swift
  4. Teste de unidade na arquitetura Clean Swift (você está aqui)
  5. Um exemplo de uma arquitetura simples de loja online Clean Swift

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


All Articles