Boas vindas a mais um curso de Clojure: explorando testes!
Neste curso, falaremos e escreveremos bastante sobre testes, fazendo comentários a medida que exploramos cada um definindo o modelo e a lógica usando ou não esquema, redefinindo e implementando novos códigos ao longo do trabalho.
Neste processo, veremos diversas maneiras de tentar garantir um comportamento de sucesso em nosso sistema, garantindo que o codigo que implementamos não apresente bugs e que funcione bem para os casos elaborados.
Falaremos sobre Test Driven Development (TDD) e Test Driven Design, bem como Boundary Tests e separação de testes por categoria ou Category Partition. Veremos sobre a validação de testes através de esquemas e adição de certos contratos antes e depois da execução de uma função.
Tentaremos garantir o comportamento de diversas maneiras possíveis, usando ferramentas nativas de Clojure ou Java, biblioteca da parte de testes da linguagem, esquema e trechos de código para compor uma bateria de testes, sejam eles automatizados, esquemas como validações na hora de invocar funções ou ainda contratos em execução ativadas ou não, sempre visando a melhoria do código.
Abordaremos criticamente as maneiras como realizamos e pensamos testes, como temos ilusão do funcionamento destes e de que funções são mais simples do que imaginamos deixando de testar erroneamente, abrindo espaço para bugs.
Portanto, tem muita coisa para vermos sobre testes. Vamos lá!
Dando início ao curso, abra o IntelliJ IDEA e crie um novo projeto clicando em "Create New Project" de Clojure com Leiningen. Em seguida, clique em "Next", depois selecione a pasta de sua preferência em "Project location" e nomeie como "hospital". Clique em "Finish" para gerar o novo projeto.
Na aba lateral, encontramos os diretórios criados. Dê duplo clique em "hospital > src > hospital > core.clj" e abra a nova aba de hospital.core
. Nesta, a definição de foo
não será usada, logo podemos apagar deixando apenas o namespace.
Criaremos alguns modelos que representam nosso hospital clicando com o botão direito sobre "src > hospital" para selecionar "New > File" e nomear como model.clj
; este é um padrão de nomenclatura em inglês bastante comum em empresas para encontrar modelos, arquivos de bancos e etc. No novo arquivo, declare o namespace (ns hospital.model)
.
Funções de lógica que trabalham com nossos modelos são criados em um outro novo arquivo chamado logic.clj
da mesma forma que fizemos com o anterior. Na nova aba, declare o namespace (ns hospital.logic)
.
Queremos criar uma função de lógica que permite adicionar pessoas em uma fila de hospital; então estabelecemos o tamanho da fila com o número máximo de 5 vagas.
Comece definindo a função cabe-na-fila?
com defn
para perguntar se cabem mais pessoas; se houver menos de 5 pessoas, é possível a entrada de mais gente, e se houver 5 ou mais, não há como incluir mais pacientes.
Para isso, escreva a implementação de cabe-na-fila?
recebendo um hospital
e o departamento
. Pegue a fila do departamento deste hospital
com ->
. Como departamento
é uma keyword, chamamos este no hospital
passando-o como parâmetro para o departamento
.
Para saber quantas pessoas estão dentro do departamento, use count
para verificar com get
hospital
e departamento
. Agora, queremos saber se há 5 pessoas nesta fila, pois se houver, não cabem mais pacientes. Portanto, deve ser not=
para 5.
Aparentemente há bug em nosso código, mas descobriremos mais adiante. Comente a linha que contém count
por enquanto.
A partir desta lógica, geraremos testes para garantir que o sistema funcione. Uma maneira é realmente escrever o código antes, e outra é escrever os testes automatizados antes do código.
(ns hospital.logic)
(defn cabe-na-fila?
[hospital departamento]
;(count (get hospital departamento))
(-> hospital
departamento
count
(not= 5)))
No desenvolvimento voltado a testes, aparecem alguns termos como Test Driven Development ou Test Driven Design (TDD), significando que os testes indicam o caminho do código, diferente do que estamos fazendo até então, pois escrevemos a função cabe-na-fila?
antes, como acreditamos que seria chamada.
O estudo de autoria do Maurício Aniche, instrutor de alguns cursos da Plataforma Alura mostra qual caminho é melhor para ter menos bugs acerca dessa temática. Nesta pesquisa, é mostrado que a qualidade do código não é tão influenciada pela utilização de testes antes da implementação quanto é influenciada pela própria experiência da desenvolvedora ou desenvolvedor. Portanto, utilizar TDD e Test First não necessariamente garante o bom funcionamento do programa.
Então, a maneira como implementamos o código apresenta bug, mas ter escrito o teste antes também não garantiria a qualidade. De qualquer forma, faremos o teste para avaliar o resultado.
Em Clojure, quando criamos um projeto com Leiningen já há um diretório chamado "test" contendo "hospital", o qual possui um arquivo de namespace chamado core_test.clj
que receberá os testes do módulo core.clj
.
Abra o core_test.clj
para ver que já há um conteúdo escrito. Apague o bloco de deftest
, pois não o usaremos, deixando apenas o namespace e os recursos importados com :require
.
Na pasta "hospital" dentro de "test", clique com o botão direito para selecionar "New > File" e gerar um novo arquivo de teste da lógica. Atente ao uso padrão de underline ao invés de ponto na nomenclatura neste caso, ficando logic_test.clj
. Já no namespace, use hífen na declaração, escrevendo (ns hospital.logic-test
.
De volta à aba h.core-test
, temos alguns imports para fazer que devem ser observados neste arquivo; é comum usar :require
no namespace de teste e naquele a ser testado.
Em h.logic-test
, ao invés de ficarmos inserindo o prefixo da biblioteca do recurso que iremos utilizar constantemente no programa, importamos usando :require
de clojure.test
para logo em seguida referenciar pelo nome com :refer
para todo conteúdo escrevendo, :all
.
Ao clicar em clojure.test
, acessamos todos os recursos defs
presentes que podemos utilizar.
Então, definiremos um teste em h.logic-test
: com deftest
, defina cabe-na-fila?
. Em seguida, use testing
- também presente em clojure.test
- para testar uma string
escrevendo "Que cabe na fila"
. Dentro, queremos que seja igual a 1
e 1
com is
para um primeiro teste simples que deveria funcionar.
Em nosso caso, rodamos project.clj
clicando com o botão direito sobre este na lista lateral para escolher "Run 'REPL for hospital'". Assim que o REPL for carregado, podemos rodar os testes.
(ns hospital.logic-test
(:require [clojure.test :refer :all]))
(deftest cabe-na-fila?
(testing "Que cabe na fila"
(is (= 1 1))))
Encontramos os comandos de REPL tanto em "Run" quanto em "Tools" na barra superior de opções, mas para nossa intenção acesse "Tools > REPL > Run tests in current NS in REPL" e observe o retorno na janela lateral que apresenta um teste sendo rodado e uma asserção relativa ao is
sem alertas de falha, significando que nosso programa está funcionando como esperado.
Agora, podemos implementar o teste real. Pegue o hospital
que tem uma fila vazia escrevendo :espera
seguido de []
entre colchetes no lugar de 1
. Tentando colocar um paciente nesta fila, pegue o hospital
novamente e chame cabe-na-fila?
para :espera
, já devolvendo verdadeiro ou falso, logo retire =
da sentença.
Desta forma, deveria caber mais pacientes. Rode no REPL clicando em "Tools > REPL > Run Tests in current NS in REPL" novamente.
(ns hospital.logic-test
(:require [clojure.test :refer :all]))
(deftest cabe-na-fila?
(testing "Que cabe na fila"
(is (cabe-na-fila? {:espera []}, :espera))))
O erro apresentado acima de toda a exceção é de aridade, ou seja, chamamos a função errada. Se observarmos em hospital.logic
, a função cabe-na-fila?
recebe dois argumentos: hospital
e departamento
.
De volta ao h.logic-test
, clique sobre cabe-na-fila?
com a tecla "Ctrl" ou "Command" apertada para ver sua definição. Com isso, o sistema não nos direciona para o arquivo de lógica e sim para a definição nesta mesma aba.
Isso acontece porque deftest
define um símbolo. Logo, definimos um símbolo chamado cabe-na-fila?
e o invocamos no bloco, mas queremos que seja invocada a função do hospital.logic
.
Para isso, precisamos importar também hospital.logic
referindo tudo que estiver público com :refer :all
para podermos acessar todas as funções e realizar nosso teste, evitando a necessidade de inserir prefixo constantemente a cada invocação.
(ns hospital.logic-test
(:require [clojure.test :refer :all]
[hospital.logic :refer :all]))
(deftest cabe-na-fila?
(testing "Que cabe na fila"
(is (cabe-na-fila? {:espera []}, :espera))))
Porém, ao tentarmos rodar desta forma com o mesmo caminho que fizemos, o REPL retorna um erro de sintaxe apontando que cabe-na-fila?
já existe e refere hospital.logic/cabe-na-fila?
, e não podemos definir duas vezes o mesmo símbolo em locais diferentes em um mesmo namespace.
Então, para evitar mais erros deste tipo, usamos por padrão cabe-na-fila?-test
em deftest
, lembrando que este mesmo padrão pode variar de acordo com a empresa. Rode novamente para analisar o resultado.
O sistema não indica mais problemas, significando que coube na fila.
Para evitar rodar o código dessa forma toda vez, vá na barra superior de opções e clique em "IntelliJ IDEA > Preferences...". Na janela "Preferences", temos o item "Keymap" na lista lateral. Queremos procurar justamente o comando "Tools > REPL > Run Tests in current NS in REPL"; no campo de busca, escreva "REPL" para acessar as ocorrências.
No resultado da pesquisa, busque por "Tools > REPL > Run Tests in current NS in REPL" e dê duplo clique para escolher a opção "Add Keyboard Shortcut" e assim gerar um atalho do teclado deste comando.
Um sugestão é apertar as teclas "Shift + Ctrl + T" ou "Command + Shift + T" ou ainda "Ctrl + Command T", o que preferir. É importante gerar um atalho que ainda não exista no campo aberto da caixa de diálogo "Keyboard Shortcut".
Definido o atalho do teclado de sua preferência, clique em "OK" na caixa de diálogo e depois em "OK" novamente na janela "Preferences". Assim, podemos rodar automaticamente ao invés de seguir todo o caminho anterior.
Insira mais um teste em deftest
com testing
, escrevendo "Que não cabe na fila quando a fila está cheia"
. Isso significa que is
not
cabe-na-fila?
quando passamos uma :espera
que já possui cinco pessoas dentro, ou seja, 1 2 3 4 5
entre colchetes.
Depois, indique que queremos colocar mais pessoas na fila de :espera
e teste rodando no REPL com o atalho.
(deftest cabe-na-fila?-test
(testing "Que cabe na fila"
(is (cabe-na-fila? {:espera []}, :espera))))
(testing "Que não cabe na fila quando a fila está cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera)))))
O REPL indica que há um teste com duas asserções rodando sem problemas, ou seja, nosso código está funcionando como planejado. Se retirarmos uma das pessoas da fila e rodarmos novamente, o REPL aponta um teste com duas asserções e uma falha.
No código, ao posicionar o cursor sobre a implementação recém alterada, o sistema avisa que esperava que não coubesse mas recebeu true
, não sendo o que queríamos. Corrija o teste adicionando novamente o paciente que foi retirado.
Para mostrar o bug e a discussão inicial sobre o teste, se estivéssemos escrito o teste antes, escreveríamos exatamente este teste que estamos trabalhando. Então, teríamos implementado o código presente em hospital.logic
e seria apresentado o mesmo bug de agora.
Existe um bug deste código atual que não está sendo testado, e é preciso analisar a fundo para localizá-lo. Existem métodos formais para fazer esta análise e definir quais testes devem ser criados que serão discutidos adiante.
Criamos um teste para o vazio []
, o qual é um caso importante. Mas também criamos para uma situação de cheio [1 2 3 4 5]
, o qual é um caso de borda exatamente em seu limite de vagas.
Façamos um teste para um cenário onde há 6 pessoas na fila; dentro de deftest
, escreva "Que não cabe na fila quando tem mais do que uma fila cheia"
com testing
. Em seguida, insira is
e not
para cabe-na-fila?
uma :espera
que tenha 6 pacientes na fila, pedindo para incluir mais pessoas com :espera
.
Rode no REPL para analisar o resultado.
(deftest cabe-na-fila?-test
(testing "Que cabe na fila"
(is (cabe-na-fila? {:espera []}, :espera))))
(testing "Que não cabe na fila quando a fila está cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera)))))
(testing "Que não cabe na fila quando tem mais do que uma fila cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera)))))
O retorno do REPL aponta que mesmo tendo ultrapassado o limite de vagas na fila, é permitida a entrada de mais pacientes, o que não é esperado. Isso aconteceu pois quando implementamos o código utilizamos diferente de 5 ou not= 5
presente na definição de cabe-na-fila?
em hospital.logic
.
Logo, todos os casos passariam sem falhas, até mesmo o 5
que devolve false
. Porém, 6
também passaria já que é diferente de 5
, e aí está o bug. O total de pessoas deve ser alterado para < 5
em cabe-na-fila?
de hospital.logic
.
Teste novamente o código de h.logic-test
para ver que todos os testes passaram. Note que, se estivéssemos escrito os testes antes, teríamos feito apenas os dois primeiros e não o terceiro.
Como Maurício Aniche aponta em sua pesquisa, existe o problema do Oráculo: fazendo testes antes, como saber o que testar se ainda não sabemos o que escrever para ser testado?
Aqui em nosso caso, seguimos um método de testes formal e bem definido na comunidade onde testamos as bordas ou Boundary Tests, um pouco acima e um pouco abaixo dela, chamados de one off. Como no primeiro teste que testamos o zero como borda do vazio, o segundo que é a borda da fila cheia no limite de cinco pessoas e o terceiro que é ultrapassando a lotação por um paciente a mais.
(deftest cabe-na-fila?-test
;borda do zero
(testing "Que cabe na fila"
(is (cabe-na-fila? {:espera []}, :espera)))
;borda do limite
(testing "Que não cabe na fila quando a fila está cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera))))
;one off da borda do limite para cima
(testing "Que não cabe na fila quando tem mais do que uma fila cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera)))))
Se estamos sendo formais, quando criamos nossos testes temos algo como um checklist a ser seguido que nos ajuda a verificar os limites do programa. Depois de vários trabalhos utilizando esse método, acaba por ser feito naturalmente, e claro que existem outras abordagens possíveis, e é aí que também entra a questão da experiência da desenvolvedora ou desenvolvedor.
Seguindo essa metodologia, vemos que outros testes devemos fazer para nosso código. Falta-nos fazer pelo menos dois: um deles com quatro pessoas na fila e que pode receber mais pacientes e outro com duas pessoas que também podem receber mais pacientes.
(deftest cabe-na-fila?-test
;borda do zero
(testing "Que cabe na fila"
(is (cabe-na-fila? {:espera []}, :espera)))
;borda do limite
(testing "Que não cabe na fila quando a fila está cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera))))
;one off da borda do limite para cima
(testing "Que não cabe na fila quando tem mais do que uma fila cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera))))
(testing "Que cabe na fila quando tem pouco menos do que uma fila cheia"
(is (cabe-na-fila? {:espera [1 2 3 4]}, :espera)))
(testing "Que cabe na fila quando tem pouca gente na fila"
(is (cabe-na-fila? {:espera [1 2]}, :espera))))
Rodando no REPL, o sistema não aponta falhas.
Porém, não necessariamente estes testes de Boundary Tests e one offs são suficientes para garantir o funcionamento do código, mas é uma ferramenta importante para o trabalho, entre outras abordagens existentes. Ter um check list também é essencial para evitar bugs, seja na criação de testes, uso de variáveis e funções, etc.
Neste passo, veremos mais uma abordagem de teste.
Em nosso código, repare que criamos testes separados uns dos outros. O primeiro é relativo à borda do zero, ou seja, da fila vazia. O segundo diz respeito ao número máximo de pessoas possíveis na fila. O terceiro apresenta a ultrapassagem do limite em um paciente.
Os dois últimos que contém apenas is
representam filas com certo número de indivíduos mas sem atingir a lotação máxima, cabendo mais pessoas e estando dentro da borda. Portanto, podem estar agrupadas sob o mesmo testing
.
(deftest cabe-na-fila?
;borda do zero
(testing "Que cabe na fila"
(is (cabe-na-fila? {:espera []}, :espera)))
;borda do limite
(testing "Que não cabe na fila quando a fila está cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera))))
;one off da borda do limite para cima
(testing "Que não cabe na fila quando tem mais do que uma fila cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera))))
;dentro das bordas
(testing "Que cabe na fila quando tem pouco menos do que uma fila cheia"
(is (cabe-na-fila? {:espera [1 2 3 4]}, :espera))
(is (cabe-na-fila? {:espera [1 2]}, :espera)))
Rodando este código no REPL, o retorno aponta um teste com 5 asserções e nenhuma falha. Ainda assim não garantimos o pleno funcionamento do programa com estes testes, sendo necessárias outras abordagens para essa garantia.
Observando o texto de hospital.logic
, percebemos mais um caso a ser testado. Tudo o que fazemos no código pode ter bordas; quando fizemos uma comparação < 5
e count
com o caso do zero, descobrimos mais bordas. No caso de departamento
também, pois ainda não existe em h.logic-test
.
Trataremos o caso da inexistência do departamento
. No código de teste, insira mais um "Que não cabe quando o departamento não existe"
com testing
, querendo que o sistema nos devolva nulo com not
.
Passe uma fila de :espera
com quatro pacientes, ou seja, cabendo mais pessoas nesta fila. Porém, peça para o setor de :raio-x
que ainda não existe, consequentemente gerando um retorno de valor false
.
(deftest cabe-na-fila?-test
;borda do zero
(testing "Que cabe na fila"
(is (cabe-na-fila? {:espera []}, :espera)))
;borda do limite
(testing "Que não cabe na fila quando a fila está cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5]}, :espera))))
;one off da borda do limite para cima
(testing "Que não cabe na fila quando tem mais do que uma fila cheia"
(is (not (cabe-na-fila? {:espera [1 2 3 4 5 6]}, :espera))))
;dentro das bordas
(testing "Que cabe na fila quando tem pouco menos do que uma fila cheia"
(is (cabe-na-fila? {:espera [1 2 3 4]}, :espera))
(is (cabe-na-fila? {:espera [1 2]}, :espera)))
(testing "Que não cabe quando o departamento não existe"
(is (not (cabe-na-fila? {:espera [1 2 3 4]}, :raio-x)))))
Ao rodar no REPL, o sistema apresenta uma falha.
De volta ao arquivo de lógica, temos o macro de threading ->
que, quando pega o departamento
, este devolve nulo. Se chamarmos uma chave para :raio-x
em um mapa inexistente, o retorno é nulo.
Se pegarmos o count
de nil
, o REPL devolve zero. Isso acontece porque várias funções de Clojure seguem a linha de pensamento do criador original de que uma sequência vazia e nulo têm certas ligações. Como em seq
vazia pode ser nula, então o count
dessa seq
deve devolver zero.
Portanto, se a devolução for zero, indica que cabem mais pessoas na fila sem que nem exista esta sessão no departamento
de hospital
, e precisamos tratar este caso.
O que podemos fazer é verificar. Vimos que podemos usar let
para extrair a fila
como um get
do departamento
do hospital
. Vimos também o if-let
para se tiver o departamento
, nos devolver o bloco de ->
. Do contrário, o if
devolve nulo e a parte hospital departamento
já é a própria fila.
(ns hospital.logic)
(defn cabe-na-fila?
[hospital departamento]
(if-let [fila (get hospital departamento)]
(-> fila
count
(< 5))))
Com este código, volte ao h.logic-test
para testar esta nova alteração. Rode no REPL para ver que a falha não é apresentada novamente.
Com tudo funcionando, podemos refatorar o código de hospital.logic
; podemos mudar if-let
para when-let
que a execução no REPL apresentaria o mesmo resultado.
Mas existe uma outra maneira de fazer o código anterior, o qual threadeava pelos valores hospital
, departamento
, count
, < 5
e etc. Porém, no processo tivemos nil
, e como nulo é importante em qualquer linguagem e possui um tratamento especial por várias funções, pode ser que o comportamento da função seguinte não apresente o que queríamos, que é exatamente nosso caso. Então, apague a linha de if-let
.
O count
tem um comportamento nulo que não é o planejado; o que queremos com nil
em count
é parar e devolver nulo. Também, é interessante que o código vá threadiando enquanto os valores não são nulos para devolver quando terminar. Se algum deles for, o programa pára e devolve nil
.
Existe a macro de threading some->
que possui essa ação que queremos. Altere ->
para some->
e execute no REPL o código de h.logic-test
.
(ns hospital.logic)
(defn cabe-na-fila?
[hospital departamento]
(some-> hospital
departamento
count
(< 5)))
Tudo funciona corretamente pois o departamento
é nulo, mostrando a eficiência desta nova abordagem.
É interessante deixar as três abordagens ->
, when-let
e some->
usadas no arquivo de lógica para serem usadas em trabalhos futuros como registro, comentando e anotando os problemas nas que apresentaram problemas.
O código usado com ->
possui uma vantagem ou desvantagem - depende do projeto - por ser menos explícito e por qualquer um que der nulo devolve nil
em todas as situações dos threadings. Em geral estes serão menores, pois se forem muito grandes significa muitos elementos na mesma função e seria necessário quebrar para testar melhor.
Mas já que o código está funcionando com a versão que possui some->
, volte ao h.logic-test
para refatorar, criar mais tipos e classes de testes. Note que só a borda em uma única variável não é interessante, sendo necessário testar uma segunda variável com outro tipo de valor que estávamos passando para a função, não sendo só uma questão do estado da fila do hospital
do departamento
, mas também uma questão própria do hospital
.
Logo, cada alteração e nova implementação em código pode ter erros e bordas para uma nova categoria de testes.
O curso Clojure: explorando testes possui 162 minutos de vídeos, em um total de 34 atividades. Gostou? Conheça nossos outros cursos de Clojure em Programação, ou leia nossos artigos de Programação.
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.