Enquanto trabalhava no projeto mais recente, fui confrontado com o teste de um aplicativo móvel conectado no nível da lógica de negócios a vários serviços de terceiros. Testar esses serviços não fazia parte da minha tarefa, no entanto, problemas com a API bloquearam o trabalho do próprio aplicativo - os testes não caíram por causa de problemas internos, mas por causa da inoperabilidade da API, mesmo antes de se verificar a funcionalidade necessária.
Tradicionalmente, os suportes são usados para testar essas aplicações. Mas eles nem sempre funcionam normalmente, e isso interfere no trabalho. Como solução alternativa, usei o moki. Eu quero falar sobre esse caminho espinhoso hoje.

Para não tocar no código de um projeto real (no NDA), para maior clareza de discussões, criei um cliente REST simples para Android, que permite o envio de solicitações HTTP (GET / POST) para um determinado endereço com os parâmetros necessários. Vamos testar.
É possível fazer o
download do código do aplicativo cliente, despachantes e testes
no GitLab .
Quais são as opções?
Havia duas abordagens para zombar no meu caso:
- implantar um servidor simulado na nuvem ou em uma máquina remota (se estamos falando de desenvolvimentos confidenciais que não podem ser levados para a nuvem);
- inicie o servidor simulado localmente - diretamente no telefone em que o aplicativo móvel está sendo testado.
A primeira opção não é muito diferente da bancada de testes. De fato, é possível alocar uma estação de trabalho na rede para o servidor simulado, mas ele precisará ser suportado, como qualquer teste. E aqui é necessário encontrar as principais armadilhas dessa abordagem. A estação de trabalho remota morreu, parou de responder, algo mudou - você precisa monitorar, alterar a configuração, ou seja, faça o mesmo que com o apoio de uma bancada de testes regular. Não podemos corrigir a situação por nós mesmos, e certamente levará mais tempo do que qualquer manipulação local. Então, especificamente no meu projeto, era mais conveniente aumentar o servidor simulado localmente.
Escolhendo um servidor simulado
Existem muitas ferramentas diferentes. Tentei trabalhar com vários e em quase todo mundo me deparei com certos problemas:
- Mock-server , wiremock - dois servidores simulados que eu não conseguia executar normalmente no Android. Como todos os experimentos ocorreram como parte de um projeto ao vivo, o tempo para a escolha foi limitado. Depois de cavar com eles alguns dias, desisti de tentar.
- Restmock é um invólucro sobre okhttpmockwebserver , que será discutido em mais detalhes posteriormente. Parecia bom, ele começou, mas o desenvolvedor deste wrapper escondeu "por baixo do capô" a capacidade de definir o endereço IP e a porta do servidor simulado, e para mim foi fundamental. O reinício foi iniciado em alguma porta aleatória. Examinando o código, vi que, quando o servidor foi inicializado, o desenvolvedor usou um método que definia a porta aleatoriamente se não a recebesse na entrada. Em princípio, era possível herdar desse método, mas o problema estava no construtor privado. Como resultado, recusei o invólucro.
- Okhttpmockwebserver - tendo tentado ferramentas diferentes, parei no servidor de simulação, que normalmente se reunia e começava localmente no dispositivo.
Analisamos o princípio do trabalho
A versão atual do okhttpmockwebserver permite implementar vários cenários de trabalho:
- Fila de respostas . As respostas simuladas do servidor são adicionadas à fila FIFO. Não importa qual API e qual caminho eu acessarei, o servidor simulado se revezará lançando mensagens nessa fila.
- O expedidor permite criar regras que determinam qual resposta dar. Suponha que uma solicitação veio de uma URL que contém um caminho, por exemplo / get-login /. Neste servidor / get-login / mock e fornece uma resposta única e predefinida.
- Solicitar Verificador . Com base no cenário anterior, posso verificar as solicitações que o aplicativo envia (nas condições especificadas, uma solicitação com determinados parâmetros realmente sai). No entanto, a resposta não é importante, pois é determinada pela forma como a API funciona. Este script implementa o verificador de solicitações.
Considere cada um dos cenários com mais detalhes.
Fila de resposta
A implementação mais simples do servidor simulado é a fila de resposta. Antes do teste, determino o endereço e a porta em que o servidor simulado será implantado, bem como o fato de ele funcionar com o princípio de uma fila de mensagens - FIFO (primeiro a entrar, primeiro a sair).
Em seguida, execute o servidor simulado.
class QueueTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.enqueue(MockResponse().setBody("1st message")) mockServer.enqueue(MockResponse().setBody("2nd message")) mockServer.enqueue(MockResponse().setBody("3rd message")) mockServer.start(ip, port) } @Test fun queueTest() { sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("1st message") returnFromResponseActivity() sendPostRequest("http://localhost:8080/getMessage") assertResponseMessage("2nd message") returnFromResponseActivity() sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("3rd message") returnFromResponseActivity() } }
Os testes são escritos usando a estrutura do Espresso, projetada para executar ações em aplicativos móveis. Neste exemplo, seleciono os tipos de solicitação e os envio por vez.
Depois de iniciar o teste, o servidor simulado fornece respostas de acordo com a fila prescrita e o teste passa sem erros.
Implementação do Dispatcher
Um expedidor é um conjunto de regras pelas quais um servidor simulado opera. Por conveniência, criei três expedidores diferentes: SimpleDispatcher, OtherParamsDispatcher e ListingDispatcher.
Simpledispatcher
Okhttpmockwebserver fornece a classe
Dispatcher()
para implementar o expedidor. Você pode herdar dela, substituindo a função de
dispatch
do seu próprio jeito.
class SimpleDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { if (request.method == "GET"){ return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request" }""") } else if (request.method == "POST") { return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request" }""") } return MockResponse().setResponseCode(200) } }
A lógica neste exemplo é simples: se um GET chegar, retornarei uma mensagem de que esta é uma solicitação GET. Se POST, retornarei uma mensagem sobre a solicitação POST. Em outras situações, retorno uma solicitação vazia.
SimpleDispatcher
aparece no teste - um objeto da classe
SimpleDispatcher
, que eu descrevi acima. Além disso, como no exemplo anterior, o servidor simulado é iniciado, apenas desta vez é indicado um tipo de regra para trabalhar com esse servidor simulado - o mesmo expedidor.
As fontes de teste com o
SimpleDispatcher
podem ser encontradas
no repositório .
OtherParamsDispatcher
Substituindo a função de
dispatch
, eu posso retirar de outros parâmetros de solicitação para enviar respostas:
class OtherParamsDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { return when { request.path.contains("?queryKey=value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request with query parameter queryKey equals value" }""") request.body.toString().contains("\"bodyKey\":\"value\"") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request with body parameter bodyKey equals value" }""") request.headers.toString().contains("header: value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was some request with header equals value" }""") else -> MockResponse().setResponseCode(200).setBody("""{ Wrong response }""") } } }
Nesse caso, demonstro várias opções para as condições.
Primeiramente, você pode passar parâmetros para a API na barra de endereços. Portanto, posso colocar uma condição na entrada de qualquer pacote
“?queryKey=value”
no caminho, por exemplo,
“?queryKey=value”
.
Em segundo lugar, essa classe permite que você entre no corpo do corpo de solicitações POST ou PUT. Por exemplo, você pode usar
contains
executando primeiro
toString()
. No meu exemplo, a condição é acionada quando uma solicitação POST contendo
“bodyKey”:”value”
. Da mesma forma, posso validar o
header : value
da solicitação (
header : value
).
Para exemplos de testes, eu recomendo consultar
o repositório .
ListingDispatcher
Se necessário, você pode implementar uma lógica mais complexa - ListingDispatcher. Da mesma maneira, eu substituo a função de
dispatch
. No entanto, agora, na classe, defino o conjunto padrão de
stubsList
(
stubsList
) - mok para diferentes ocasiões.
class ListingDispatcher: Dispatcher() { private var stubsList: ArrayList<RequestClass> = defaultRequests() @Override override fun dispatch(request: RecordedRequest): MockResponse = try { stubsList.first { it.matcher(request.path, request.body.toString()) }.response() } catch (e: NoSuchElementException) { Log.e("Unexisting request path =", request.path) MockResponse().setResponseCode(404) } private fun defaultRequests(): ArrayList<RequestClass> { val allStubs = ArrayList<RequestClass>() allStubs.add(RequestClass("/get", "queryParam=value", "", """{ "message" : "Request url starts with /get url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "queryParam=value", "", """{ "message" : "Request url starts with /post url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Request url starts with /post url and body contains bodyParam:value" }""")) return allStubs } fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) } fun addMockStub(stub: RequestClass) { stubsList.add(stub) } }
Para fazer isso, criei uma classe aberta
RequestClass
, todos os campos vazios por padrão. Para esta classe, defino uma função de
response
que cria um objeto
MockResponse
(retornando uma resposta 200 ou alguma outra
responseText
) e uma função de
matcher
que retorna
true
ou
false
.
open class RequestClass(val path:String = "", val query: String = "", val body:String = "", val responseText: String = "") { open fun response(code: Int = 200): MockResponse = MockResponse() .setResponseCode(code) .setBody(responseText) open fun matcher(apiCall: String, apiBody: String): Boolean = apiCall.startsWith(path)&&apiCall.contains(query)&&apiBody.contains(body) }
Como resultado, posso criar combinações mais complexas de condições para stubs. Esse design me pareceu mais flexível, embora o princípio em sua essência seja muito simples.
Mas, acima de tudo, nessa abordagem, gostei de poder substituir alguns stubs imediatamente, se houver necessidade de alterar alguma coisa na resposta do servidor simulado em um teste. Ao testar grandes projetos, esse problema surge com bastante frequência, por exemplo, ao verificar alguns cenários específicos.
A substituição pode ser feita da seguinte maneira:
fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) }
Com esta implementação do expedidor, os testes permanecem simples. Também inicio o servidor simulado, apenas selecione o
ListingDispatcher
.
class ListingDispatcherTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) private val dispatcher = ListingDispatcher() @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.setDispatcher(dispatcher) mockServer.start(ip, port) } . . . }
Para o experimento, substituí o stub pelo POST:
@Test fun postReplacedStubTest() { val params: HashMap<String, String> = hashMapOf("bodyParam" to "value") replacePostStub() sendPostRequest("http://localhost:8080/post", params = params) assertResponseMessage("""{ "message" : "Post request stub has been replaced" }""") }
Para fazer isso, chame a função
replacePostStub
de um
dispatcher
regular e
replacePostStub
uma nova
response
.
private fun replacePostStub() { dispatcher.replaceMockStub(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Post request stub has been replaced" }""")) }
No teste acima, verifiquei se o stub foi substituído.
Em seguida, adicionei um novo esboço, que não estava no padrão.
@Test fun getNewStubTest() { addSomeStub() sendGetRequest("http://localhost:8080/some_specific_url") assertResponseMessage("""{ "message" : "U have got specific message" }""") }
private fun addSomeStub() { dispatcher.addMockStub(RequestClass("/some_specific_url", "", "", """{ "message" : "U have got specific message" }""")) }
Solicitar Verificador
O último caso - Verificador de solicitação - não fornece espionagem, mas verifica as solicitações enviadas pelo aplicativo. Para fazer isso, inicio o servidor de simulação implementando o expedidor para que o aplicativo retorne pelo menos alguma coisa.
Ao enviar uma solicitação de um teste, ela chega ao servidor simulado. Por meio dele, eu posso acessar os parâmetros de solicitação usando
takeRequest()
.
@Test fun requestVerifierTest() { val params: HashMap<String, String> = hashMapOf("bodyKey" to "value") val headers: HashMap<String, String> = hashMapOf("header" to "value") sendPostRequest("http://localhost:8080/post", headers = headers, params = params) val request = mockServer.takeRequest() assertEquals("POST", request.method) assertEquals("value", request.getHeader("header")) assertTrue(request.body.toString().contains("\"bodyKey\":\"value\"")) assertTrue(request.path.startsWith("/post")) }
Acima, mostrei o teste com um exemplo simples. Exatamente a mesma abordagem pode ser usada para JSON complexo, inclusive para verificar toda a estrutura da solicitação (você pode comparar no nível JSON ou analisar JSON em objetos e verificar a igualdade no nível do objeto).
Sumário
Em geral, gostei da ferramenta (okhttpmockwebserver) e a uso em um projeto grande. Claro, há algumas coisinhas que eu gostaria de mudar.
Por exemplo, não gosto que você precise digitar o endereço local (localhost: 8080 em nosso exemplo) nas configurações do seu aplicativo; talvez ainda consiga encontrar uma maneira de configurar tudo para que o servidor simulado responda ao tentar enviar uma solicitação para qualquer endereço.
Além disso, não tenho a capacidade de redirecionar solicitações - quando o servidor simulado envia uma solicitação adicional, se não tiver um stub adequado para ela. Não existe essa abordagem nesse servidor simulado. No entanto, nem chegou à sua implementação, pois no momento o projeto de "combate" não tem essa tarefa.
Autor do artigo: Ruslan Abdulin
PS Publicamos nossos artigos em vários sites do Runet. Assine nossas páginas no
canal VK ,
FB ou
Telegram para descobrir todas as nossas publicações e outras notícias do Maxilect.