Olá, gostaríamos de dar as boas-vindas a mais um curso de iOS na Alura com Ândriu Coelho.
Audiodescrição: Ândriu é uma pessoa branca de olhos e cabelos castanhos. Tem barba e bigode e usa uma camiseta azul escuro com o logotiop da Alura. Ao fundo, parede branca e lisa sem decorações.
A ideia é continuar o desenvolvimento do aplicativo Chef Delivery, que já começamos a testar no curso anterior. Queremos avançar a escrita dos testes com classes mais complexas.
Vamos entender como testamos classes que possuem dependências. Nesse momento, vamos introduzir os conceitos de stub (imitador) e mock (simulador), além aprender a como injetar um imitador via dependência.
A ideia é que a classe não saiba qual é a classe de produção ou qual é o imitador que estamos usando. Para isso, também iremos trabalhar com protocolos.
Depois, vamos aprender como testar métodos assíncronos, ou seja, métodos que demoram para responder. E abordaremos como fazer o assert (afirmação) nesse tipo de método.
Vamos avançar também falando sobre variáveis que têm a keyword (palavra-chave) Published
, ou seja, variáveis que podem emitir ou receber eventos. Assim, entenderemos como testar esse tipo de variável.
Por último, vamos falar também sobre testes da camada de serviço, a camada que faz de fato a requisição para o servidor e devolve a resposta. Como testamos esse tipo de classe? Vamos usar uma biblioteca para nos ajudar nesse tipo de teste.
Como pré-requisito, é importante que você tenha familiaridade com a linguagem Swift, que saiba trabalhar com layouts usando SwiftUI e que saiba também fazer requisições HTTP.
Esperamos você na primeira aula!
Para iniciar nosso curso, vamos continuar com o desenvolvimento do projeto Chef Delivery, que já começamos no curso anterior.
Mesmo que você tenha feito o curso anterior de testes, é importante que você faça o download do projeto novamente, pois fizemos pequenos ajustes no projeto. O projeto estará disponível para download nas atividades do curso.
Já havíamos escrito alguns testes simples para o Chef Delivery, que foram úteis para estudarmos os fundamentos dos testes de unidade. Porém, a partir de agora, a ideia é aprofundar nossos conhecimentos sobre testes.
Na classe SearchStoreViewModel
, já havíamos escrito um teste para o filteredStores()
, que é o método que filtra os restaurantes e os estabelecimentos na lista. Testamos o fluxo comum, que é o caminho feliz, e também o fluxo de exceção, caso ocorra algum erro.
SearchStoreViewModel.swift
:
class SearchStoreViewModel: ObservableObject {
// MARK: - Attributes
let service = SearchService()
@Published var storesType: [StoreType] = []
@Published var searchText: String = ""
init() {
fetchData()
}
// MARK: - Class methods
func fetchData() {
Task {
do {
let result = try await service.fetchData()
switch result {
case .success(let stores):
self.storesType = stores
case .failure(let error):
print(error.localizedDescription)
}
} catch {
print(error.localizedDescription)
}
}
}
func filteredStores() throws -> [StoreType] {
if searchText.isEmpty {
return storesType
}
let filteredList = storesType.filter { $0.matches(query: searchText.lowercased()) }
if filteredList.isEmpty {
throw SearchError.noResultsFound
}
return filteredList
}
}
A ideia é continuar explorando essa classe de ViewModel inicialmente. Além do método filteredStores()
, também temos um método que é o fetchData()
, que é um método que chama a camada de serviço para buscar, de fato, os restaurantes em uma requisição HTTP.
Estamos usando o Apiary para simular a resposta de uma requisição, já que não temos um back-end próprio para nosso projeto. Basicamente, temos a View, que se conecta com o ViewModel, o qual chama a camada de serviço, que, por sua vez, faz a requisição e devolve a lista de restaurantes para o ViewModel.
O ViewModel possui uma variável na linha 19 chamada storesType
, que é a lista de restaurantes. Assim que recebemos a resposta do servidor com os restaurantes, como é uma variável @Published
, ela fica escutando qualquer alteração e notifica a View quando chegam esses novos valores.
Queremos continuar o estudo dos nossos testes a partir de agora, testando o próximo método dessa classe do ViewModel, que é o método fetchData()
.
fetchData()
Já temos um arquivo de teste, que é o SearchStoreViewModelTest.swift
, localizado no target de teste. Vamos abrir esse arquivo para começar a escrever esse novo teste.
Logo após a linha 132, após o método testFilteredStoresException()
, vamos escrever um novo método de teste. Vamos começar declarando uma função através da palavra-chave func
. Todo método de teste começa com o prefixo test
, seguido do nome do método, que será FetchDataWithSuccess
.
Entre as chaves, vamos pegar a referência para a camada de serviço usando um let searchService
, que é do tipo SearchService
, e já o instanciamos.
Depois, teremos os resultados da requisição armazenados em let result
. Como é uma Throwing Function, vamos declarar um try
. É uma função que também usa async-await, então vamos aguardar a resposta da requisição com o await
. Em seguida, chamamos a camada de serviço através de searchService.fetchData()
.
O único detalhe é que, como usamos o async-await, precisamos marcar o método como async
. Além disso, como é uma Throwing Function, devemos marcar a assinatura do método com essa keyword throws
.
SearchStoreViewModelTest.swift
:
func testFetchDataWithSuccess() async throws {
let searchService = SearchService()
let result = try await searchService.fetchData()
}
Em seguida, vamos fazer um switch-case para verificar qual foi o resultado dessa requisição. Caso seja sucesso, teremos a lista de lojas e restaurantes, ou seja, case .success(let stores)
.
Podemos fazer um assert nesse case
. Faremos um XCTAssertFalse()
, passando stores.IsEmpty
, pois a lista não pode estar vazia.
Também existe o caso de falha, onde teremos um erro. Para isso, basta escrever case .failure(let error)
.
Também faremos um assert para verificar se o teste falhou nesse momento. Com ajuda do XCTFail()
, informaremos que foi um "Erro ao fazer uma requisição".
func testFetchDataWithSuccess() async throws {
let searchService = SearchService()
let result = try await searchService.fetchData()
switch result {
case .success(let stores):
XCTAssertFalse(stores.isEmpty)
case .failure(let error):
XCTFail("Erro ao fazer uma requisição")
}
}
Vamos conferir se o código funciona? Vamos rodar todos os testes para fazer uma primeira validação.
O primeiro teste de unidade que fizemos passou!
Duas novidades nesse teste em relação à assinatura do método:
async
no início do método;throws
ao método, permitindo o uso de try
dentro do teste.Qual é o objetivo desse teste? Estamos na camada do ViewModel que faz a chamada da camada de serviço para buscar os restaurantes. Nesse teste, estamos validando se a requisição traz, de fato, a lista com os restaurantes que é o que nos interessa para devolver para a View.
Só isso nos traz alguns problemas. Escrevemos esse teste justamente para explorar essa lógica.
Primeiro, devemos pensar muito bem em relação à unidade que estamos testando. Queremos testar o comportamento dessa classe do ViewModel, a SearchStoreViewModel
.
Contudo, repare que em nenhum momento chamamos o sut
, o qual utilizamos em todos os outros métodos de teste. Lembre-se que o declaramos logo no início da classe:
final class SearchStoreViewModelTests: XCTestCase {
// MARK: - Attributes
var sut: SearchStoreViewModel!
// código omitido…
}
O que é o
sut
? É o System Under Test (Sistema Sob Teste). Na verdade, é a unidade que estamos testando no nosso código.
Estamos testando o SearchStoreViewModel
, e não a camada de serviço, como declaramos na função testFetchDataWithSuccess()
.
Na verdade, estamos ultrapassando o limite da unidade que estamos testando. Embora o ViewModel faça referência com a camada de serviço, ele é apenas uma dependência do ViewModel. Mas, nesse caso, estamos testando a camada de serviço.
Outro ponto muito importante é que estamos fazendo a requisição de verdade, quando chamamos o fetchData()
. Isso não é interessante em teste de unidade. Na verdade, evitamos fazer requisições para a API - ao invés disso, utilizamos mocks e stubs, que é o que vamos estudar mais adiante nesse curso.
Em suma, queríamos chamar atenção a esse ponto entre a unidade que estamos testando e uma outra unidade. Não podemos ultrapassar essa fronteira da unidade. Mas como vamos testar o ViewModel, já que ele depende da camada de serviço?
Vamos analisar rapidamente a dependência. Para isso, apertamos a tecla "Command" e clicamos em SearchStoreViewModel
na declaração do var sut
. Isso abre a classe de produção.
Na linha 18, temos já a referência dessa dependência:
SearchStoreViewModel.swift
:
class SearchStoreViewModel: ObservableObject {
// MARK: - Attributes
let service = SearchService()
// código omitido…
}
Será que não conseguimos, de alguma forma, usar algo que imite essa dependência, um imitador, para que possamos trocar entre usar a classe de produção e algo que simule uma resposta para usar para testes?
Acabamos de criar o primeiro teste de unidade, o testfetchDataWithSuccess()
. No entanto, estávamos refletindo sobre a unidade a ser testada. Estamos falando da classe SearchStoreViewModel
, que é a classe de produção para a qual estamos criando os testes de unidade.
O primeiro ponto importante que descobrimos é que essa classe possui uma dependência. Qual é a dependência? A camada de serviço em let service = SearchService()
. Estamos testando uma unidade que depende de outra para conseguir realizar a sua operação. No caso, buscar os dados na camada de serviço.
Estamos testando o método fetchData()
e o que nos interessa é testar o comportamento desse método, e não da camada de serviço, que parece ser algo parecido.
Vamos entrar na camada de serviço, por exemplo. O SearchService
também possui um método fetchData()
, o que pode confundir um pouco no início, porque, na verdade, temos o ViewModel como ponte entre a View e a camada de serviço. Então, o ViewModel chama o Service, o qual devolve a lista de restaurantes.
SearchService.swift
:
struct SearchService {
func fetchData() async throws -> Result<[StoreType], RequestError> {
guard let url = URL(string: "https://private-11274d-chefdeliveryapi.apiary-mock.com/search") else {
return .failure(.invalidURL)
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
let (data, _) = try await URLSession.shared.data(for: request)
let storesObjects = try JSONDecoder().decode([StoreType].self, from: data)
return .success(storesObjects)
}
}
Essa é outra unidade que não vamos testar no momento - vamos focar apenas no SearchStoreViewModel
.
Primeiro ponto importante é que criar uma classe e instanciá-la junto com a propriedade, como é o caso de let service = SearchService()
, pode dificultar a criação de testes. Isso ocorre porque a classe se torna dependente dessa instância específica.
Para facilitar os testes, vamos aprender uma técnica que chamamos de injeção de dependências para injetar tanto essa classe SearchService
, quanto alguma outra que possa imitar o comportamento dessa classe.
Assim, vamos poder passar por parâmetro a classe de verdade ou também passar por parâmetro uma classe que vai imitar o comportamento da classe de verdade.
Quando estamos codando o nosso projeto, devemos tomar cuidado com esse tipo de abordagem, onde declaramos uma propriedade e já a instanciamos na própria classe. Como podemos melhorar isso?
Primeiro, podemos colocar trocar o sinal de igual (=
) por dois-pontos (:
) após a propriedade service
. Segundo, vamos retirar os parênteses de SearchService
para não instanciá-la.
Ao invés disso, vamos receber essa classe por parâmetro no construtor. Isso vai melhorar a implementação. Em init()
, receberemos service
que é do tipo SearchService
.
Dentro do construtor, pegaremos o self.service
da classe e diremos que ele é igual ao service
que estamos recebendo por parâmetro.
SearchStoreViewModel.swift
:
class SearchStoreViewModel: ObservableObject {
// MARK: - Attributes
let service: SearchService
@Published var storesType: [StoreType] = []
@Published var searchText: String = ""
init(service: SearchService) {
self.service = service
fetchData()
}
// código omitido…
}
Após fazer essa alteração, vamos precisar acessar o ContentView
para passar o SearchService
por parâmetro na linha 46. Afinal, se tentarmos buildar o projeto com "Command + B" nesse momento, ocorre um erro e o XCode nos informa que faltam argumentos para o parâmetro service
.
Após passar o SearchService
por parâmetro e instanciá-lo, podemos dar um "Command + B" novamente.
ContentView.swift
:
// código omitido…
SearchStoreView(viewModel: SearchStoreViewModel(service: SearchService()))
.tabItem {
Image(systemName: "magnifyingglass")
Text("Busca")
}
// código omitido…
Já melhoramos o código, porém ainda não resolvemos o problema em sua totalidade, porque, mais uma vez, estamos recebendo o tipo concreto da classe SearchService
no construtor do SearchStoreViewModel
.
O que podemos fazer para melhorar? Vamos abrir a camada de serviço, a SearchService
, para criar um protocolo, que é o ponto-chave que queríamos mostrar para você nesse vídeo.
Como podemos deixar o nosso código mais genérico usando o protocolo?
Vamos declarar um protocol
, que vai se chamar SearchServiceProtocol
. Esse protocolo vai ter um método que será exatamente a assinatura do método fetchData()
que já temos na classe de produção. Basta copiar toda a assinatura do método e colar dentro do protocolo.
E o que isso vai permitir? Ao invés de passar o SearchService
por parâmetro, poderemos passar algo que implemente esse protocolo por parâmetro, que é o SearchServiceProtocol
.
Na declaração da struct SearchService
, vamos acrescentar dois-pontos e implementar o protocolo SearchServiceProtocol
. A obrigação de quem se conforma a esse protocolo é implementar o método fetchData()
. Nesse caso, esse método já está implementado na struct.
SearchService.swift
:
protocol SearchServiceProtocol {
func fetchData() async throws -> Result<[StoreType], RequestError>
}
struct SearchService: SearchServiceProtocol {
func fetchData() async throws -> Result<[StoreType], RequestError> {
// código omitido…
}
}
Agora, na classe de produção do ViewModel, o SearchStoreViewModel
, ao invés de receber uma classe concreta no construtor, vamos receber algo que implemente o protocolo SearchServiceProtocol
.
E aí, vamos alterar também a declaração do let service
que será também do tipo SearchServiceProtocol
.
SearchStoreViewModel.swift
:
class SearchStoreViewModel: ObservableObject {
// MARK: - Attributes
let service: SearchServiceProtocol
@Published var storesType: [StoreType] = []
@Published var searchText: String = ""
init(service: SearchServiceProtocol) {
self.service = service
fetchData()
}
// código omitido…
}
O que ganhamos com isso? Agora, vamos poder passar tanto a classe produtiva quanto alguma struct ou classe criada na classe de teste que implementa esse protocolo.
Só que para teste, como havíamos comentado, não faz sentido fazer requisição para API de verdade. O ideal é simular, ou seja, imitar o comportamento da classe de produção e trazer esses objetos já pré-definidos.
Então, ao invés de fazer uma requisição para o servidor, já ensinamos à classe o que ela deve retornar em caso de teste para que consiga testar o comportamento da ViewModel.
A seguir, continuamos escrevendo o teste de unidade e usando todos esses ajustes que acabamos de fazer.
O curso iOS: aplicando testes de unidade com mocks, stubs e outros frameworks possui 138 minutos de vídeos, em um total de 47 atividades. Gostou? Conheça nossos outros cursos de iOS em Mobile, ou leia nossos artigos de Mobile.
Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.