Design Patterns para melhorar seus testes, Parte 1: Builder e Fluent Interfaces

Este post é o início de uma série de posts sobre design patterns (ou padrões de projeto) que podem muito bem ser utilizados nos seus testes automatizados (em qualquer nível - unidade, integração ou UI).

A ideia dessa série de posts é abordar alguns padrões de forma simples, que possam ser implementados facilmente e melhorar a qualidade do código dos seus testes.

Nesta primeira parte da série, vou falar sobre o padrão Builder, um pattern bastante conhecido para a criação de objetos mais complexos. Também veremos um pouco sobre Fluent Interfaces ao longo dos exemplos.


Mas peraí, o que são Design Patterns mesmo?

O livro "Design Patterns com Java: Projeto orientado a objetos guiados por padrões", do Eduardo Guerra, possui uma definição bem sucinta para Design Patterns:

Um padrão é uma solução consolidada para um determinado problema em um contexto.

É imprescindível citar o livro "Design Patterns: Elements of Reusable Object-Oriented Software", que iniciou a disseminação de padrões de projeto na comunidade de desenvolvimento de software. Esse livro é conhecido também como GoF (Gang of Four, referente aos quatro autores do livro).


O padrão Builder

O padrão Builder é muito útil quando lidamos com a construção de objetos complexos, com construtores que requerem muitos parâmetros ou com múltiplos construtores. Veremos que o padrão também é útil quando temos métodos com parâmetros demais (ao ponto de dificultar a manutenção do código).

Neste post, vamos ver uma abordagem do padrão seguindo a linha do que foi descrito no livro "Effective Java", do Joshua Bloch. Essa abordagem também é feita no ótimo post do Dustin Marx sobre o pattern.

Vale lembrar que não existe uma maneira "certa" ou "errada" de se implementar o padrão. No final do post, na parte de Referências, estão alguns posts que gostei sobre o assunto, e é possível ver implementações ligeiramente diferentes, mas com o mesmo propósito.

O importante é que o design pattern resolva um problema e melhore seu código. :)


Alternativas

Dependendo do seu cenário, existem alternativas ao Builder, que podem ser mais simples. Duas alternativas muito simples são: Custom Types e Parameters Object. Ambas são bem explicadas na série de posts Too Many Parameters in Java Methods, do Dustin Marx (clique nos nomes para abrir os links).

Basicamente, a primeira consiste em criar tipos customizados para seus parâmetros, enquanto a segunda consiste em agrupar parâmetros em diferentes objetos.


Como o Builder funciona?

Vamos considerar como exemplo a classe Usuario a seguir:

public class Usuario  
{
    private String nome;
    private String cpf; 
    private Calendar dataNascimento;
    private String endereco;
    private String bairro;
    private String cidade;
    private String estado;
    private String informacoes;

    public Usuario(String nome, String cpf,
        Calendar dataNascimento, String endereco,
        String bairro, String cidade,
        String estado, String informacoes)
    {
        this.nome = nome;
        this.cpf = cpf;
        this.dataNascimento = dataNascimento;
        this.endereco = endereco;
        this.bairro = bairro;
        this.cidade = cidade;
        this.estado = estado;
        this.informacoes = informacoes;
    }
}

Observe o construtor da classe, veja como fica complicado escrever tantos parâmetros. É aí que o Builder vai nos ajudar. :)

O Builder será uma classe separada responsável pela construção do nosso objeto. Conforme mencionei anteriormente, não existe uma maneira certa ou errada para escrever esse Builder, então você pode optar pelo que preferir: escrever uma classe em outro arquivo .java OU criar uma classe interna (nested class). Neste exemplo, vou escrever uma classe interna.

A classe Usuario, juntamente com a classe interna UsuarioBuilder ficaria da seguinte forma:

public class Usuario  
{
    //atributos obrigatorios
    private String nome;
    private String cpf;
    //atributos opcionais
    private String dataNascimento;
    private String endereco;
    private String bairro;
    private String cidade;
    private String estado;
    private String informacoes;

    private Usuario(UsuarioBuilder builder)
    {
        this.nome = builder.nome;
        this.cpf = builder.cpf;
        this.dataNascimento = builder.dataNascimento;
        this.endereco = builder.endereco;
        this.bairro = builder.bairro;
        this.cidade = builder.cidade;
        this.estado = builder.estado;
        this.informacoes = builder.informacoes;
    }

    public static class UsuarioBuilder
    {
        private String nome;
        private String cpf;
        private String dataNascimento;
        private String endereco;
        private String bairro;
        private String cidade;
        private String estado;
        private String informacoes;

        public UsuarioBuilder(String nome, String cpf)
        {
            this.nome = nome;
            this.cpf = cpf;
        }

        public UsuarioBuilder dataNascimento(String dataNascimento)
        {
            this.dataNascimento = dataNascimento;
            return this;
        }

        public UsuarioBuilder endereco(String endereco)
        {
            this.endereco = endereco;
            return this;
        }

        public UsuarioBuilder bairro(String bairro)
        {
            this.bairro = bairro;
            return this;
        }

        public UsuarioBuilder cidade(String cidade)
        {
            this.cidade = cidade;
            return this;
        }

        public UsuarioBuilder estado(String estado)
        {
            this.estado = estado;
            return this;
        }

        public UsuarioBuilder informacoes(String informacoes)
        {
            this.informacoes = informacoes;
            return this;
        }

        public Usuario build()
        {
            return new Usuario(this);
        }
    }
}

O exemplo é grande, mas fique calmo: o código é bem simples. Repare que os atributos obrigatórios (nome e cpf) foram passados no construtor do Builder. Isso garante com que o objeto criado esteja sempre em um estado válido.

Ok, mas como uso esse Builder? Como vou criar um usuário?

Veja o método abaixo, que retorna um usuário, utilizando o Builder que criamos:

public Usuario getUsuario()  
{
    return new Usuario.UsuarioBuilder("Dollynho", "11133344409")
        .dataNascimento("01/09/1979")
        .endereco("Rua do Amiguinho, 1000")
        .bairro("Guaranalandia")
        .cidade("Guaranazinho do Sul")
        .estado("Acre")
        .informacoes("Cuidado com o sol!")
        .build();
}

Perceba como ficou muito mais simples ler e criar nosso objeto. O builder possui uma Fluent Interface para tornar o código mais legível. Recomendo muito ler o excelente artigo do Martin Fowler sobre Fluent Interfaces caso queira saber mais sobre o assunto.


Exemplo: Preenchimento de formulário extenso em um Page Object

Para demonstrar um possível uso do pattern em testes automatizados, considere o seguinte contexto: você possui testes de UI automatizados (com Selenium WebDriver + Page Objects) para o seu sistema de Cadastro de Usuários.

Em um determinado momento, imagine que seu Page Object (CriarUsuarioPage.java) tenha um método para preencher o form de criação de usuários parecido com esse:

public void criarUsuario(String nome, String cpf,  
    String dataNascimento, String endereco,
    String bairro, String cidade,
    String estado, String infos)
{
    driver.findElement(By.id("nome")).sendKeys(nome);
    driver.findElement(By.id("cpf")).sendKeys(cpf);
    driver.findElement(By.id("dataNasc")).sendKeys(dataNascimento);
    //idem para outros campos...

    //submit do form
    driver.findElement(By.id("btn-submit")).click();
}

Um teste que use esse método seria:

@Test
public void testeCriarUsuario()  
{
    CriarUsuarioPage page = new CriarUsuarioPage(driver);

    page.criarUsuario("Fulaninho", "88177766624",
         "01/01/1951", "Av. Arantes 90", "Copacabana",
         "Rio de Janeiro", "Rio de Janeiro", "Perto do metro Cantagalo");
}

Com oito parâmetros, o código já não fica tão agradável de ser lido. Imagine se fossem mais parâmetros?

É possível implementar um Builder para o preenchimento desse form, seguindo a ideia do exemplo anterior. Uma implementação simples poderia ser:

public class CriarUsuarioPage extends BasePage  
{
    //atributos, construtor, métodos...

    public static class CriarUsuarioFormBuilder
    {   
        private WebDriver driver;

        public CriarUsuarioFormBuilder(WebDriver driver, String nome, String cpf)
        {
            this.driver = driver;
            sendKeys("nome", nome);
            sendKeys("cpf", cpf);
        }

        public CriarUsuarioFormBuilder dataNascimento(String dataNascimento)
        {
            sendKeys("nasc", dataNascimento);
            return this;
        }

        public CriarUsuarioFormBuilder endereco(String endereco)
        {
            sendKeys("endereco", endereco);
            return this;
        }

        //idem para demais campos...

        public void submit()
        {
            this.driver.findElement(By.id("btn-submit")).click();
        }

        private void sendKeys(String elementId, String text)
        {
            this.driver.findElement(By.id(elementId)).sendKeys(text);
        }
    }
}

Dessa forma, um teste que use esse builder poderia ser:

@Test
public void criarUsuarioPreenchendoTodosOsCampos()  
{
    new CriarUsuarioPage.CriarUsuarioFormBuilder(driver, "Admin2", "12332199912")
        .dataNascimento("01/07/1918")
        .endereco("Av Presidente Vargas 1000")
        .bairro("Centro")
        .cidade("Rio de Janeiro")
        .estado("Rio de Janeiro")
        .infosAdicionais("Perto da Central do Brasil")
        .submit();

    assertTrue(usuarioPage.getMsgSucesso().isDisplayed());
}

Bem mais claro, não? Uma abordagem comumente utilizada na construção de Page Objects é a de ter um método para interagir com cada campo. Cada método desses poderia retornar a própria instância (this), o que permitiria o encadeamento, nos remetendo ao conceito de Fluent Interface mencionado anteriormente. Um exemplo de teste nesse estilo seria:

@Test
public void criarUsuarioPreenchendoTodosOsCamposFluent()  
{
    CriarUsuarioPage page = new CriarUsuarioPage(driver);

    page.nome("Admin2")
        .cpf("12332199912")
        .dataNascimento("01/07/1918")
        .endereco("Av Presidente Vargas 1000")
        .bairro("Centro")
        .cidade("Rio de Janeiro")
        .estado("Rio de Janeiro")
        .infosAdicionais("Perto da Central do Brasil")
        .submit();

    assertTrue(usuarioPage.getMsgSucesso().isDisplayed());
}

Veja o projeto de exemplo no GitHub contendo o código de todos os exemplos do post: https://github.com/stefanteixeira/exemplo-patterns


Dicas

  • Use abstrações

Criar builders e interfaces fluentes ajudam muito na legibilidade, mas imagine se tivéssemos o exemplo anterior com 20 parâmetros, ele seria tão legível assim? É sempre importante pensar em abstrações que possam simplificar o seu código.

Abstrações como Custom Types e Parameters Object ajudam muito na legibilidade e manutenção. Considere o uso dessas ou de demais abstrações. O post do Dustin Marx explica a construção de builders usando essas abstrações.

  • Cuidado com o estado válido dos objetos

Preste atenção em criar sempre objetos com estados válidos. No caso da classe Usuario, só teríamos um objeto válido se o mesmo tivesse os atributos nome e cpf.

No nosso exemplo de preenchimento de form, o estado válido da ação é quando temos os campos nome e cpf preenchidos.


Vantagens e desvantagens

Uma clara vantagem é a melhora da legibilidade e manutenibilidade do código com o padrão Builder. Entretanto, para conseguirmos isso, tivemos que escrever mais linhas de código. Somando isso ao fato do Java ser uma linguagem mais verbosa, acabamos escrevendo uma boa quantidade de código para algo simples.

Porém, escrever um pouco mais de código vale a pena pelo retorno que o pattern nos oferece. Já lidei em diversos projetos com métodos contendo mais de 10 parâmetros, por exemplo, e o esforço gasto para ler e manter código assim é bastante custoso.

Vale lembrar uma citação ótima do Uncle Bob Martin (criador do livro Clean Code), que fala sobre o tempo gasto lendo x escrevendo código:

“The ratio of time spent reading (code) versus writing is well over 10 to 1 ... (therefore) making it easy to read makes it easier to write.”


Conclusão

Uma possível dúvida que você pode ter tido é: "E quando eu tenho muitos campos obrigatórios? Como o Builder me ajudaria?"

Essa pergunta será respondida na próxima parte da série, na qual vou falar sobre um tipo mais sofisticado de Builder, ideal para esse caso: o Step Builder.

Criei um projeto no GitHub para colocar o código-fonte de todos os exemplos. O projeto foi criado com o Maven e, para importá-lo, basta ter o Eclipse com o plugin do Maven instalado e fazer um "File -> Import... -> Maven -> Existing Maven Projects". Crie um fork ou baixe o projeto e faça seus testes. :)

Para terminar, não esqueça de conferir os links abaixo, na seção de Referências. Caso surja alguma dúvida ou sugestão, não hesite em entrar em contato ou fazer um comentário!

Abraços e até o próximo post :)


Referências

Posts sobre o padrão Builder
http://www.javaworld.com/article/2074938/core-java/too-many-parameters-in-java-methods-part-3-builder-pattern.html
http://www.javacodegeeks.com/2013/01/the-builder-pattern-in-practice.html
http://rwhansen.blogspot.com.br/2007/07/theres-builder-pattern-that-joshua.html

Outras alternativas (Custom Types e Parameters Object)
http://www.javaworld.com/article/2074932/core-java/too-many-parameters-in-java-methods-part-1-custom-types.html
http://www.javaworld.com/article/2074935/core-java/too-many-parameters-in-java-methods--part-2--parameters-object.html

StackOverflow e outros
http://stackoverflow.com/questions/328496/when-would-you-use-the-builder-pattern
http://stackoverflow.com/questions/7302891/the-builder-pattern-and-a-large-number-of-mandatory-parameters
http://martinfowler.com/bliki/FluentInterface.html


Sobre o autor: Stefan Teixeira trabalha como QA Engineer e, desde o final de 2014, tem se aventurado no mundo DevOps. É Bacharel em Ciência da Computação pela UFRJ e MBA em Garantia de Qualidade de Software pela Escola Politécnica da UFRJ. Entusiasta de Testes Automatizados (e de tudo que possa ser automatizado!), Agile Testing e da cultura DevOps.

Contatos: stefanfk@gmail.com | Twitter | LinkedIn


comments powered by Disqus