É muito comum principalmente em Aplicações funcionais como React e React Native, utilizarmos funções dentro de uma função principal. Mas o que há de errado com isso? Não conseguirmos acessá-las de forma isolada, diferente de funções contidas em Classes. É aí que quero chegar.
Levando isso em consideração, é obvio que é muito mais difícil escrever testes unitários de forma isolada para testar sua aplicação. Portanto, queria compartilhar com vocês uma forma de desacoplar sua aplicação contendo cenários onde somos "obrigados" a adicionar funções dentro de uma função principal, e assim conseguir testá-las de forma isolada utilizando completion handler's.
- 🤔 O porquê desse artigo?
- 🤓 Como cheguei na solução?
- 😎 Desacoplando o código
- 🤯 Testando as funções de forma isolada
🤔 O porquê desse artigo?
Particularmente já observei muito esse cenário de acoplamento em funções envolvendo a navegação no react native. Isso aconteceu comigo recentemente, onde eu queria testar algumas funções isoladamente que continham regras de navegação. Ao tentá-las retirar da função principal e isolá-las para poder testá-las, a aplicação quebrava devido a dependência do navigation.
Vou tentar exemplicar isso através do código:
export default function NotificationListener(navigation: any) {
const navigateToChat = (id: any, content: any) => {
if (id && content) {
navigation.navigate('Chat', { id: id, content: content })
}
}
const goToScreen = (notification: any) => {
const {
userId,
userName,
userMessage
} = notification.data
const content = { userName, userMessage }
navigateToChat(userId, content)
}
}
Após me deparar com esse código, e a necessidade de manté-lo testável, decidi que deveria investir esforços para desacopla-lo e conseguir testá-lo. E a solução foi utilizar completion handler's.
🤓 Como cheguei na solução?
Como de fato o que eu realmente queria testar era a regra que permitia a chamada das ações de navegação, e era o que estava me impedindo de desacoplar esse código... Então a primeira medida a ser tomada foi desacoplar o que estava em volta da ação de navegação.
Com isso, a solução se tornou fácil, bastava eu passar via parâmetro uma função que conclua com a navegação.
😎 Desacoplando o código
Antes de começarmos a refatorar o código, vamos ver um passo a passo de como fiz isso:
- Criei um arquivo separado chamado NotificationListenerActions, que seria o responsável por ter todos os tipos de ações a serem tomadas ao receber uma notificação.
- Movi as funções que realizavam algum tipo de navegação, para o arquivo NotificationListenerActions e as deixei visíveis exportando-as.
- Adicionei um parâmetro a mais em todas as funções que finalizavam com a chamada desse parâmetro.
- Por fim, em NotificationListener, chamei essas funções e passei via parâmetro a função que concluia com a ação de navegação.
O arquivo NotificationListenerActions ficou mais ou menos assim:
export const navigateToChat = (id: any, content: any, navigation: any) => {
if (id && content) {
navigation.navigate('Chat', { id: id, content: content })
}
}
E o arquivo NotificationListener ficou assim:
export default function NotificationListener(navigation: any) {
const goToScreen = (notification: any) => {
const {
userId,
userName,
userMessage
} = notification.data
const content = { userName, userMessage }
navigateToChat(userId, content, navigation)
}
}
E foi nesse momento que a aplicação quebrou 😵💫! Como já mencionei, precisaremos desacoplar a navegação das nossas funções, ou seja, do navigateToChat. Então agora vamos utilizar um completion handler para fazer isso.
export const navigateToChat = (id: any, content: any, completionHandler: (router: string, params: { id: number, content: {userName: string, userMessage: string} }) => void) => {
if (id && content) {
completionHandler('Chat', { id: id, content: content })
}
}
Como pode-se observar, agora em nenhum momento o navigation é mencionado na nossa função navigateToChat. O primeiro passo foi dado, agora vamos chamar essa função corretamente no NotificationListener .
export default function NotificationListener(navigation: any) {
const navigateTo = (router: string, params: any) => {
navigation.navigate(router, params)
}
const goToScreen = (notification: any) => {
const {
userId,
userName,
userMessage
} = notification.data
const content = { userName, userMessage }
navigateToChat(userId, content, navigateTo)
}
}
Explicando um pouco o que fizemos acima. Se você observar, não mudamos muita coisa, apenas o fato de que agora recebemos uma função via parâmetro e que dentro dela realizamos a ação de navegação.
🤯 Testando as funções de forma isolada
E agora finalmente podemos testar nossas regras contidas nas funções de forma isolada. Então vamos ver como ficaria o teste unitário para esse cenário.
describe('NotificationListenerActions', () => {
test('should not call completionHandler if id and content is empty', () => {
const completionHandler = jest.fn()
const id = ''
const content = {}
const sut = navigateToChat(id, content, completionHandler)
expect(completionHandler).not.toHaveBeenCalled()
})
test('should call completionHandler if id and content is not empty', () => {
const completionHandler = jest.fn()
const id = 'any_id'
const content = { userName: 'any_userName' userMessage: 'any_userMessage' }
const sut = navigateToChat(id, content, completionHandler)
expect(completionHandler).toHaveBeenCalledTimes(1)
})
})
Como você pode observar, a proposta dos testes unitários não foram testar as ações de navegação, mas sim as regras contidas para realizar tal navegação.
Portanto, através dessa abordagem se torna possível testar as regras contidas nas funções de forma isolada e assim sentir-se seguro na hora da refatoração e implementação do código, nesse caso, ao receber um Push Notification.
É isso aí galera, nos vemos na próxima!
Top comments (1)
Show demais! Ótimo aprendizado.