BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos JUnit 5 - Test Drive - Parte 1

JUnit 5 - Test Drive - Parte 1

Pontos Principais

  • JUnit 5 chegou;
  • É um modelo de API evoluída e extensível que melhorou consideravelmente "a ferramenta JUnit";
  • A arquitetura modular torna o "JUnit uma plataforma" disponível para outros frameworks de testes;
  • Foi reescrita completamente, mas pode coexistir com versões antigas utilizando o mesmo código base.

Uma pequena equipe de desenvolvedores dedicados está trabalhando no JUnit 5, a nova versão de uma das bibliotecas mais populares do Java. Enquanto as melhorias na superfície são em grande maioria incrementais, a verdadeira inovação acontece por debaixo dos panos, com o potencial de redefinir os testes na JVM.

Depois do protótipo em Novembro de 2015 e uma versão alfa em Fevereiro de 2016, em Julho foram lançados os Milestone 1 e Milestone 2 e este artigo apresenta os testes realizados com esta versão! A versão final do JUnit 5 foi lançado em Setembro de 2017, um pouco após a escrita original deste artigo em inglês.

Nesta primeira parte, vamos descobrir como escrever os testes, entender todas as pequenas melhorias que a nova versão traz, discutir por que a equipe JUnit decidiu que era hora da reescrita, e ver como a nova arquitetura pode ser um divisor de águas para a execução de testes na JVM.

A segunda parte vai abordar em detalhes como executar os testes, apresentar alguns dos novos recursos interessantes do JUnit e demonstrar como as funcionalidades principais podem ser estendidas.

Escrevendo os testes

Vamos começar com as dependências e ver como podemos rapidamente criar alguns testes.

Configuração em cinco segundos

Teremos uma visão mais detalhada sobre a instalação e arquitetura mais tarde. Por agora, simplesmente importe esses artefatos com sua ferramenta de gerenciamento de dependências preferida. Este exemplo foi feito com Gradle:

org.junit.jupiter:junit-jupiter-api:5.0.0-M2
org.junit.jupiter:junit-jupiter-engine:5.0.0-M2
org.junit.platform:junit-platform-runner:1.0.0-M2

Isto é o que tem hoje, o suporte completo do JUnit vem por aí, e só será necessário importar o artefato junit-jupiter-api.

A seguir, temos uma classe para escrever os testes utilizando o JUnit 5:

package com.infoq.junit5;

import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
public class JUnit5Test {
    // testes aqui
}

Agora está tudo configurado para escrever os testes com JUnit 5 e já temos a IDE ou ferramenta para executá-lo.

Um exemplo simples

Para a execução de testes simples, não mudou quase nada:

@RunWith(JUnitPlatform.class)
public class JUnit5Test {
    @BeforeAll
    static void inicializarRecursosExternos() {
   	 System.out.println("Inicializando os recursos externos...");
    }

    @BeforeEach
    void inicializarObjetosMock() {
   	 System.out.println("Inicializando objetos mock...");
    }

    @Test
    void algumTeste() {
   	 System.out.println("Executando algum teste...");
   	 assertTrue(true);
    }

    @Test
    void outroTeste() {
   	 assumeTrue(true);

   	 System.out.println("Executando outro teste...");
   	 assertNotEquals(1, 42, "Porque estes valores não são iguais?");
    }

    @Test
    @Disabled
    void disabilitarTeste() {
   	 System.exit(1);
    }

    @AfterEach
    void parando() {
   	 System.out.println("Parando...");
    }

    @AfterAll
    static void liberarRecursosExternos() {
   	 System.out.println("Liberando os recursos externos...");
    }
}

Na superfície, o JUnit 5 recebeu apenas melhorias incrementais; as funcionalidades mais revolucionárias estão sob os panos e já serão apresentados. Mas primeiro, vejamos algumas das melhorias mais óbvias desta versão.

Melhorias

Visibilidade

A mudança mais óbvia é que os métodos de testes não podem ser públicos. É necessário apenas visibilidade package (também não pode ser privado), para que possamos manter as classes de testes livres da confusão de muitas palavras-chave públicas.

Teoricamente as classes de testes também precisam ter visibilidade default. Mas, por causa da configuração simples que acabamos de fazer, as nossas ferramentas só verificam as anotações nas classes públicas. Isso mudará após as ferramentas receberem o suporte ao JUnit 5.

Teste de ciclo de vida

@Test

A anotação mais básica do JUnit 4 é @Test, usada para marcar os métodos que serão executados como testes.

A anotação foi praticamente inalterada, embora já não tenha argumentos opcionais; as exceções esperadas podem ser verificada por meio de assertions. (Para timeouts ainda não há um substituto.)

@Before e @After

Para executar os códigos de configuração, antes e depois dos testes, podemos usar @BeforeAll, @BeforeEach, @AfterEach e @AfterAll. Estas anotações possuem nomes mais apropriado para suas funções, mas semanticamente são idênticas as anotações @BeforeClass, @Before, @After e @AfterClass do JUnit 4.

Como uma nova instância é criada para cada teste, os métodos @BeforeAll / @AfterAll são chamados apenas uma vez para todos eles, como não é específico para qual teste serão utilizados, ambos devem ser declarados de modo estático (semelhante ao @BeforeClass e @AfterClass no JUnit 4).

Se métodos diferentes são anotados com as mesma anotações, a ordem de execução é indefinida.

Desativando os testes

Os testes podem simplesmente ser desativados com @disabled, o que equivale ao @Ignored do JUnit4. Este é apenas um caso especial de um Condition, que veremos mais adiante, quando abordarmos a extensão JUnit.

Assertions

Depois de tudo criado e funcionando, utilizamos as assertions para verificar o comportamento desejado. Ocorreu diversas melhorias incrementais, como:

  • As mensagens dos assertions foram movidas para o último parâmetro. Desta forma, as chamadas com ou sem mensagens ficam mais uniformes, como os dois primeiros parâmetros são sempre os valores da informação real e esperada, e o argumento opcional vem por último;
  • Usando lambdas, as mensagens dos asserts podem ser criadas se for necessária (lazy), podendo melhorar o desempenho se esta criação é uma operação demorada;
  • As verificações booleanas aceitam Predicates.

Há também o novo assertAll, que verifica um grupo de invocações que normalmente são relacionadas e, se a alguma afirmação falhar, não para as demais validações, imprime apenas as mensagens das validações que falharam, exemplo:

@Test
void assertRelatedProperties() {
    Developer dev = new Developer("Johannes", "Link");

    assertAll("developer",
   		 () -> assertEquals("Marc", dev.firstName()),
   		 () -> assertEquals("Philipp", dev.lastName())
    );
}

Este teste produz a seguinte mensagem de falha:

org.opentest4j.MultipleFailuresError: developer (2 failures)
    expected:  but was: 
    expected:  but was:

Note que apresenta a mensagem de falha para as duas validações.

Finalmente temos assertThrows e expectThrows, ambos causam a falha do teste se a exceção esperada não for lançada pelo método chamado. Mas para continuar verificando as propriedades (por exemplo, que a mensagem contém determinadas informações), o expectThrows retorna a exceção.

@Test
void assertExceptions() {
    // assert that the method under test
    // throws the expected exception */
    assertThrows(Exception.class, unitUnderTest::methodUnderTest);

    Exception exception = expectThrows(
        Exception.class,
        unitUnderTest::methodUnderTest);
    assertEquals("This shouldn't happen.", exception.getMessage());
}

Suposições

Suposições (assumptions) permitem executar testes se determinada condição for esperada. Como uma suposição, precisa ser formulada como uma expressão booleana, se a condição não for atendida o teste encerra. Isso pode ser utilizado para reduzir o tempo de execução e a verbosidade das ferramentas de testes, especialmente em casos de falha.

@Test
void exitIfFalseIsTrue() {
    assumeTrue(false);
    System.exit(1);
}

@Test
void exitIfTrueIsFalse() {
    assumeFalse(this::truism);
    System.exit(1);
}

private boolean truism() {
    return true;
}

@Test
void exitIfNullEqualsString() {
    assumingThat(
             // state an assumption (a false one in this case) ...
   		 "null".equals(null),
             // … and only execute the lambda if it is true
   		 () -> System.exit(1)
    );
}

As suposições podem ser usadas para abortar testes cujas pré condições não são atendidas (assumeTrue e assumeFalse) ou para executar partes específicas de um teste de acordo com a condição (assumingThat). A principal diferença é que os testes abortados são reportados como desativados, ao invés de ter um teste que fica verde porque não executou devido a alguma condição.

Um pouco de história

Como vimos, o JUnit 5 vem com diversas melhorias incrementais sobre como escrevemos os testes. Também traz novos recursos, que veremos mais adiante. Mas, curiosamente, a verdadeira razão por trás de tanto esforço é um pouco mais profunda.

Porque reescrever o JUnit?

JUnit e as ferramentas

As técnicas de desenvolvimento como desenvolvimento orientado a testes e integração contínua estão cada vez mais generalizadas, os testes tornaram-se cada vez mais importante para o dia-a-dia do desenvolvedor. Como tal, as solicitações de funcionalidades nas IDEs também cresceu. Os desenvolvedores queriam execuções simples e específicas (até métodos individuais), feedback rápido e fácil navegação. Ferramentas de automatização e servidores de CI também incluíram suas próprias necessidades.

Mas, como o JUnit 4 foi preparado para isso? Além de sua dependência com o Hamcrest 1.3, o JUnit 4,12 é um artefato monolítico contendo a API para os desenvolvedores escreverem os testes e a engine para execução desses testes, e é só isso. Descobrir os testes, por exemplo, teve de ser implementado novamente por todas as ferramentas de que queriam fazer isso.

Infelizmente, isso não foi suficiente para suportar algumas características das ferramentas avançadas. Os desenvolvedores de ferramentas tiveram que improvisar, usando reflection para acessar as APIs internas do JUnit, classes não-públicas, e até mesmo campos private. De repente, os detalhes de implementação que poderiam ser reformulado livremente, se tornaram de fato, partes da API pública. O resultado foi manutenção desagradável e dificuldade de fornecer melhorias.

Johannes Link, precursor da reescrita atual, chamou esse problema de "batalha de Sísifo", e resume esta situação como:

O sucesso da JUnit como uma plataforma impede o desenvolvimento de JUnit como uma ferramenta.

Estendendo o JUnit

A execução dos testes foi o mecanismo original que pode ser estendido no JUnit 4. Poderíamos criar nossa própria implementação de Runner e dizer ao JUnit para usá-lo, anotando a classe de testes com @RunWith(OurNewRunner.class). Nossa classe customizada para executar os testes precisa implementar todo ciclo de vida dos testes, incluindo instanciação, configuração e liberação, execução dos testes, tratamento de exceções, envio de notificações, etc.

Isso deixou trabalhoso e inconveniente a criação de pequenas extensões. E tinha várias limitações em que quase sempre era necessário ter um Runner por classe de teste, tornando impossível combiná-los e ter os benefícios dos Runners do Mockito e Spring ao mesmo tempo.

Para aliviar essas limitações, o JUnit 4.7 introduziu regras. A execução padrão do JUnit 4 pode encapsular o teste como um Statement e passá-lo para as regras que serão aplicadas. Então, é possível criar pastas temporária, executar teste de acionamento de eventos no Swing, ou definir o timeout para execução dos testes.

As regras foram uma grande melhoria, mas geralmente precisam executar algum código antes, durante e depois da execução do teste; além desses pontos do ciclo de vida, pouco suporte estava disponível para implementar extensões mais exigentes.

Também há o fato de que todos os casos de testes precisam ser conhecidos antes da execução iniciar. Isso impede a criação dinâmica de casos de testes, por exemplo, em resposta ao comportamento observado durante a execução do teste.

Por tanto, havia dois mecanismos de extensão concorrentes, cada um com suas próprias limitações, mas também com bastante sobreposição. Isso dificultava a criação de uma extensão limpa e simples. Além disso, foi relatado que a composição de diferentes extensões é problemática e muitas vezes não tem o desempenho esperado.

Chamadas com Lambda

O JUnit 4 tem mais de 10 anos de idade e ainda usa o Java 5, por isso ficou de fora em todas as melhorias posteriores da linguagem Java. Uma das melhorias mais notáveis é as expressões lambda, que permitiria criar códigos como:

   test(“someTest”, () -> {
   	 System.out.println("Running some test...");
   	 assertTrue(true);
    });

Equipe JUnit Lambda

Então agora entendemos a situação que conduz à ideia de uma reescrita, e em 2015 foi formada a equipe JUnit Lambda com este objetivo. A equipe foi formada por Johannes Link (que posteriormente deixou o projeto), Marc Philipp, Stefan Bechtold, Matthias Merdes, e Sam Brannen.

Patrocínio e financiamento coletivo

É interessante notar que a Andrena Objects, Namics, e Heidelberg Mobil, os empregadores de Marc Philipp, Stefan Bechtold, e Matthias Merdes, respectivamente, patrocinaram seis semanas de trabalho integral de cada um no projeto. Ficou claro, porém, que o tempo de desenvolvimento adicional e mais financiamento seria necessário para organizar um workshop inicial. Eles estimaram que precisavam de pelo menos €25.000 e começou uma campanha de financiamento coletivo no Indiegogo. Depois de um começo lento, as coisas melhoraram resultando em uma quantia de €53.937 (cerca de US$60.000).

Isto permitiu que a equipe passa-se aproximadamente dois meses de desenvolvimento em tempo integral no projeto. A propósito, a utilização desses dinheiro foi totalmente transparente.

Protótipo, versão alfa e milestones

Em Outubro de 2015, a equipe JUnit Lambda começou com um workshop em Karlsruhe, Alemanha, antes de embarcar em um mês de trabalho em tempo integral. O protótipo resultante foi lançado quatro semanas depois, demonstrando muitas das novas funcionalidades, e até mesmo alguns experimentos que não foram para a versão atual.

Depois de recolher os comentários, a equipe começou a trabalhar na próxima versão, rebatizada como JUnit 5, e lançou a versão alpha em Fevereiro de 2016. Outra rodada de comentários e cinco meses de intenso trabalho de desenvolvimento mais tarde, Milestone 1 foi lançado 07 de Julho de 2016. Duas semanas e um par de correções de bugs mais tarde, Milestone 2, nosso assunto atual, viu o luz do dia. Em Junho, o projeto sofreu uma outra transformação e JUnit 5 foi dividido em JUnit Jupiter, Plataforma JUnit e JUnit Vintage, que serão explicados a seguir.

Comentários

Com essa nova versão, o projeto está coletando os comentários novamente. A comunidade pode contribuir com o JUnit 5 abrindo issues ou pull-requests no GitHub.

A equipe e alguns dos primeiros a adotarem fizeram algumas palestras sobre JUnit 5:

  • JavaOne em San Francisco, CA; 18 de Setembro de 2017.

Próximos milestones e versão final

Depois de ter passado o patrocínio e financiamentos coletivos, a equipe voltou aos seus trabalhos, trabalhando no JUnit 5 em seu tempo livre. E eles fizeram um bom progresso! Apesar deste artigo utilizar como base o Milestone 2, a versão final do JUnit 5 foi lançado em Setembro de 2017.

Arquitetura

Vimos como a arquitetura monolítica JUnit 4 tornou o desenvolvimento difícil. Então, como a nova versão vai mudar isso?

Separando os conceitos

Um frameworks de testes com duas tarefas importantes:

  • permitir que os desenvolvedores escrevam os testes;
  • permitir que as ferramentas executem os testes.

Ponderando sobre o segundo ponto, torna-se óbvio que ele contém partes que são idênticas em diferentes frameworks de testes. Sendo o JUnit, TestNG, Spock, Cucumber, ScalaTest, etc., as ferramentas normalmente precisam de um nome e resultado do teste, uma forma de executar os testes, hierarquia de relatórios, etc.

Por que repetir o código que trata os mesmos pontos entre os diferentes frameworks? Por que as ferramentas precisam implementar um suporte específico para este ou aquele framework (e versão) se, em um nível abstrato, as funcionalidades são sempre as mesmas?

JUnit como uma plataforma

O JUnit pode ser a biblioteca Java mais utilizada e é certamente o framework de testes mais popular na JVM. Este sucesso vai de mãos dadas com uma forte integração com as IDEs e ferramentas.

Ao mesmo tempo, outros frameworks de testes estão explorando novas abordagens interessantes para os testes, embora a falta de integração muitas vezes atraia os desenvolvedores de volta para JUnit. Talvez esses frameworks possam se beneficiar do sucesso da JUnit e se apoiando sobre a integração que é fornecida? (Assim como tantas linguagens de programação se beneficiam do sucesso de Java, aproveitando a JVM.)

Migração

Mas isso não é só um argumento teórico; é importante para o próprio projeto JUnit, porque ele se conecta à questão crítica da migração. Podem existir ou serem criadas novas ferramentas para suportar as versões 4 e 5 em paralelo? Pode ser difícil convencer os fornecedores das ferramentas a adicionar tanto código, mas se não o fizerem, os desenvolvedores não teriam qualquer incentivo para atualizar seus frameworks de testes.

Se o JUnit 5 pudesse executar ambas as versões dos testes por trás de uma API uniforme, claramente será mais poderosa e conveniente, permitindo que as ferramentas possam remover a integração obsoleta com JUnit 4.

Modularização

A ideia da modularização trouxe uma arquitetura desacoplada, na qual diferentes papéis (desenvolvedores, tempos de execução, ferramentas) dependem de diferentes artefatos:

  1. uma API para os desenvolvedores escreverem seus testes;
  2. uma engine para cada API descobrir, apresentar e executar os testes correspondentes;
  3. uma API que todas as engines devem implementar para que possam ser utilizadas de forma uniforme;
  4. um mecanismo que orquestra as engines.

Isso separa "a ferramenta JUnit" (1. e 2.) da "plataforma JUnit" (3. e 4.). Para deixar essa distinção mais clara, o projeto utiliza o seguinte esquema de nomeação:

  • A nova API que vimos anteriormente é chamado JUnit Jupter. É o que os desenvolvedores terão mais contato;
  • A plataforma das ferramentas será adequadamente chamada de JUnit Platform;
  • Ainda não vimos, mas há também um subprojeto do JUnit chamado Vintage, que irá se adaptar ao testes JUnit 3 e 4 para serem executados com o JUnit 5.

O JUnit 5 é a soma destas três partes. E sua nova arquitetura é o resultado dessa distinção:

junit-jupiter-api (1)

A API que os desenvolvedores escrevem seus testes. Contém as anotações, verificações, etc. que vimos anteriormente.

junit-jupiter-engine (2)

Uma implementação do junit-engine-api (veja a seguir) que executa os testes com JUnit 5, ou seja, os testes escritos com junit-jupiter-api.

junit-platform-engine (3)

A API que todas as engine de testes devem implementar, de modo que elas serão acessíveis de uma maneira uniforme. Os engines podem executar os típicos testes JUnit ou podem optar por executar os testes escritos com TestNG, Spock, Cucumber, etc. Eles podem tornar-se disponíveis para serem executados (veja a seguir) ao se registrarem com o ServiceLoader do Java.

junit-platform-launcher (4)

Usa o ServiceLoader para descobrir as implementações das engines de testes e organizar sua execução. Fornece uma API para as IDEs e ferramentas de construção interagirem com a execução do teste, por exemplo, a execução de testes individuais e a apresentação dos resultados.

Os benefícios desta arquitetura são aparentes; agora só precisamos de mais dois componentes para executar os testes escritos com o JUnit 4 testes:

junit-4.12 (1)

O artefato do JUnit 4 funciona como a API que o desenvolvedor implementa seus testes, mas também contém a funcionalidade principal de como executar os testes.

junit-vintage-motor (2)

Uma implementação do junit-platform-engine que executa os testes escritos com JUnit 4. Pode ser visto como um adaptador de JUnit 4 para a versão 5.

Outros frameworks já fornecem a API para escrever os testes, então o que está faltando para a plena integração com JUnit 5 é uma implementação das engines de testes.

Uma imagem vale mais que mil palavras:

Ciclo de vida da API

O próximo problema resolvido foi das APIs internas que todo mundo estava usando. Portanto, a equipe criou um ciclo de vida para sua API. Aqui estão as explicações diretamente da fonte:

Interna

Não deve ser usado por códigos diferente do próprio JUnit. Pode ser removida sem aviso prévio.

Deprecated

Não deve mais ser usado, pode desaparecer na próxima versão.

Experimental

Destinado a novas funcionalidades, experimentais, na qual estamos à procura de feedback. Use com cuidado, pode ser promovido a mantido ou estável no futuro, mas também pode ser retirado sem aviso prévio.

Mantida

Destinado aos recursos que não sofreram quebra de compatibilidade, pelo menos não entre as atualizações menores de uma grande versão serão. Se algum dia for removido, primeiramente será rebaixado para Deprecated.

Estável

Destina-se aos recursos que não sofreram quebra de compatibilidade entre as versões mais recentes.

Classes com visibilidade pública serão anotadas com @API(uso), no qual o uso é um dos valores do ciclo de vida apresentados, por exemplo: @API(Stable). Isto fornecerá a quem usa as APIs uma melhor percepção do que está acontecendo, e a equipe do JUnit terá a liberdade para alterar ou remover as APIs não suportadas.

Open Test Alliance

Como vimos, a arquitetura JUnit 5 permite IDEs e ferramentas de compilação para usá-lo como uma fachada para outras estruturas de testes (assumindo que forneçam as engines correspondentes). Com esta abordagem, as ferramentas podem uniformemente descobrir, executar e avaliar os testes sem ter que implementar o suporte específico de cada framework.

As falhas nos teste são tipicamente expressas com exceções. Infelizmente as diferentes estruturas de testes e bibliotecas de verificações, geralmente não utilizam as mesmas classes, mas sim implementam suas próprias variantes (geralmente que estendem AssertionError ou RuntimeException).Isso deixava a interoperabilidade mais complexa do que o necessário e impede que as ferramentas façam um tratamento uniforme.

Para resolver este problema a equipe JUnit Lambda preparou um projeto separado, o Open Test Alliance para a JVM. Esta é a proposta:

Com base nas discussões com os desenvolvedores de IDEs e ferramentas, como: Eclipse, Gradle e IntelliJ, a equipe JUnit Lambda está trabalhando em uma proposta para um projeto open source, que fornecerá uma base comum mínima para testar as bibliotecas na JVM.

O principal objetivo do projeto é permitir que os frameworks de testes como: JUnit, TestNG, Spock, etc, e bibliotecas de terceiros, como: Hamcrest, AssertJ, etc, possam utilizar o mesmo conjunto comum de exceções. Dessa forma as IDEs e ferramentas podem suportar de forma consistente todos os cenários de teste, por exemplo, o tratamento consistente quando uma verificação ou suposição falhar, bem como a visualização da execução dos testes nas IDEs e relatórios.

Até agora, as respostas desses projetos foram em sua maioria carente. Se você, caro leitor, acha que isso é uma boa idéia, encorajamos que você solicite ao Open Test Alliance para manter os frameworks de sua preferência.

Compatibilidade

Dado que o JUnit pode executar engines de testes para as versões 4 e 5 ao mesmo tempo, um projeto pode manter testes em ambas as versões. E, de fato, o JUnit 5 usa novas estruturas de pacotes: org.junit.jupiter, org.junit.platform e org.junit.vintage. Isto significa que não haverá conflitos, quando diferentes versões JUnit forem usadas ​​em paralelo e também facilita uma migração lenta para JUnit 5.

Bibliotecas de testes como Hamcrest e AssertJ, que se comunicam com JUnit via exceções, continuarão funcionando na nova versão.

Resumo

Concluímos a primeira parte do artigo sobre JUnit 5. Criamos um ambiente para escrever e executar os testes e vimos como a superfície da API recebeu uma evolução incremental. Com isso já podemos começar a experimentar essa nova versão.

Também discutimos como nossas ferramentas estão ligadas aos detalhes da implementação do JUnit 4, de tal maneira que um novo começo era necessário. Esta não foi a única razão pela qual, o modelo de extensão do JUnit 4 é insatisfatório e o desejo de usar expressões lambdas para definir os testes também precisaria ser reescrito.

A nova arquitetura visa evitar os erros do passado. É dividido em JUnit Jupiter, a biblioteca que usamos para escrever testes e a JUnit Platform, e as ferramentas da plataforma podem ser construídas mantendo claramente separado estas duas preocupações. E também abre o sucesso do "JUnit como plataforma" para outras estruturas de testes que agora podem ser integradas.

Na segunda parte, abordaremos como os testes com JUnit 5 podem ser executados nas IDEs, ferramentas de compilação e via console. E, finalmente, veremos alguns dos recursos interessantes que esta nova versão trará, como o modelo de extensão.

Sobre o autor

Nicolai Parlog é desenvolvedor de software e entusiasta Java. Constantemente lê, pensa e escreve sobre Java, e codifica para ganhar a vida e se divertir. Contribui com diversos projetos de código aberto e blogs sobre o desenvolvimento de software no CodeFX. Você pode acompanhar Nicolai no Twitter.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT