Behavior-Driven Development na plataforma .NET

Veja neste artigo os principais conceitos da metodologia conhecida como BDD (Behavior-Driven Development), assim como de que forma o Visual Studio suporta esta abordagem de desenvolvimento.

Independente do porte ou do enfoque de cada organização, a rotina de muitos profissionais da área de desenvolvimento de software costuma apresentar certas dificuldades em comum. Pressões por uma rápida entrega, prazos extremamente curtos, equipes reduzidas, além de mudanças frequentes e repentinas nos requisitos estão entre os principais desafios no dia-a-dia de quem está engajado na implementação de soluções. Aliado a tudo isso está o fato de que nem sempre as áreas de negócio e técnica se “comunicam” numa mesma língua, bem como a constatação de que muitas vezes os testes em aplicações são relegados a um segundo plano.

Metodologias ágeis como XP (Extreme Programming) e Scrum surgiram com o objetivo de oferecer opções que fossem capazes de superar estas adversidades. Procurando conciliar questões como tempo reduzido e mudanças frequentes ao longo de um projeto, estas alternativas buscam um equilíbrio sustentável envolvendo qualidade e produtividade.

Dentre as técnicas que se popularizaram com a maior aceitação dos métodos ágeis, é possível destacar o uso de testes unitários automatizados com o intuito de validar o funcionamento de classes e métodos em aplicações. Partindo da codificação de casos que verificam uma ou mais condições estipuladas previamente, estas construções foram inclusive a base para o surgimento de uma nova abordagem conhecida como TDD (Test-Driven Development).

O desenvolvimento de soluções em TDD tem como premissa básica a codificação de testes unitários, com isto acontecendo antes mesmo da implementação das partes que serão submetidas a análises. Esta é justamente uma maneira encontrada para evitar a elaboração de checagens “viciadas”. Além disso, por priorizar uma melhor organização do código, são favorecidas práticas como separação de responsabilidades, alta coesão e baixo acoplamento.

A implementação de uma funcionalidade em TDD segue um ciclo chamado Red-Green-Refactor, cujas diferentes fases encontram-se indicadas na Figura 1. Importante mencionar que os testes unitários são executados em todos esses estágios.

Figura 1. Ciclo de desenvolvimento em TDD

Embora sejam inegáveis os benefícios da adoção de TDD em projetos de software, algumas dificuldades podem surgir com o decorrer do tempo:

Buscando superar estas limitações de TDD, o especialista em softwares Dan North propôs em 2006 uma nova abordagem chamada “Behavior-Driven Development” (BDD). A meta deste artigo é discutir os principais conceitos relacionados ao desenvolvimento segundo BDD, apresentando ainda um exemplo de implementação desta metodologia no Visual Studio 2015 (através do uso do framework SpecFlow).

Behavior-Driven Development: uma visão geral

A partir de sua experiência como instrutor ministrando treinamentos sobre TDD, Dan North se deparou com uma série de questionamentos levantados por desenvolvedores a respeito desta abordagem:

Quanto às dificuldades encontradas na adoção de TDD em projetos de software destacam-se:

Além destes aspectos vistos como entraves, outros fatores devem ser levados em conta no que se refere ao uso de testes automatizados:

Procurando conciliar todas estas constatações, Dan North propôs uma nova abordagem para testes automatizados chamada “Behavior-Driven Development” (BDD). Esta metodologia de desenvolvimento emprega “user stories” (histórias), as quais descrevem os comportamentos esperados para diversas funcionalidades de um sistema.

Do ponto de vista estrutural, uma user story é organizada de acordo com as seguintes premissas:

Na Figura 2 está a representação esquemática de uma user history em conformidade com a metodologia BDD. Os termos em azul correspondem a palavras-chave, as quais normalmente indicam ações executadas a partir de um sistema:

Figura 2. Estrutura de uma user story em BDD

O ciclo de desenvolvimento em BDD possui uma organização bastante similar àquela adotada em TDD, diferindo apenas pela elaboração de um teste de aceitação (Figura 3). Esta etapa adicional envolve a criação da user story, a qual servirá de base para a implementação e validação de uma funcionalidade no sistema considerado.

Figura 3. Ciclo de desenvolvimento em BDD

Como benefícios decorrentes do uso de BDD em projetos de software estão:

BDD e a plataforma .NET

O SpecFlow é talvez hoje a principal solução para a adoção das práticas de BDD em projetos no Visual Studio. Baseado em um framework chamado Cucumber (o qual foi concebido inicialmente para a linguagem Ruby), esta solução é utilizada em conjunto com frameworks de testes unitários como MS Test, NUnit e xUnit.net.

Do ponto de vista prático, o SpecFlow faz uso de um mecanismo conhecido como Gherkin para a interpretação das user stories. Já o Gherkin nada mais é do que um parser que emprega uma linguagem estruturada de sintaxe simples e não-técnica, possuindo inclusive suporte à internacionalização (diversos idiomas estão disponíveis para a elaboração de histórias).

A intenção por trás do uso do SpecFlow está em automatizar a execução dos cenários que compõem uma user story (em um arquivo conhecido como “feature” ou “especificação”), validando assim o comportamento de uma determinada funcionalidade. Tudo isso é possível graças à integração existente entre o SpecFlow e mecanismos para a implementação de testes unitários (como os frameworks MS Test e NUnit).

Assim como acontece em versões anteriores, o Visual Studio 2015 também oferece suporte para a execução de testes automatizados baseados no uso do SpecFlow. Para que isto seja possível acessar dentro da IDE o menu Tools e, em seguida, a opção "Extensions and Updates..." (Figura 4). Selecionar então a extensão "SpecFlow for Visual Studio 2015", como indicado na Figura 5.

Figura 4. Instalando uma extensão no Visual Studio 2015

Figura 5. Selecionando a extensão para uso do SpecFlow com o Visual Studio 2015

Exemplo de utilização do framework SpecFlow

Para implementar os projetos demonstrados neste artigo foram utilizados os seguintes recursos:

Inicialmente será criado um projeto do tipo “Class Library” chamado “ExemploBDD”, conforme apresentado na Figura 6.

Figura 6. Criando o projeto ExemploBDD

A ideia é que a biblioteca ExemploBDD contenha uma classe estática que terá por nome “ConversorTemperatura”. Este tipo será responsável por converter uma temperatura em Fahrenheit (medida de uso comum em países de língua inglesa) para o equivalente nas escalas Celsius e Kelvin (esta última costuma ser utilizada em medições e experimentos de laboratório).

Na Figura 7 está um diagrama com a representação em UML da classe ConversorTemperatura.

Figura 7. A classe ConversorTemperatura

Na Listagem 1 está a definição inicial para a classe ConversorTemperatura. O retorno dos métodos FahrenheitParaCelsius e FahrenheitParaKelvin neste primeiro momento será zero, já que a implementação desta estrutura segue uma abordagem típica de BDD (com a codificação definitiva da funcionalidade acontecendo somente após a implementação de um projeto de testes).

Listagem 1. Versão inicial da classe ConversorTemperatura

using System; namespace ExemploBDD { public static class ConversorTemperatura { public static double FahrenheitParaCelsius(double temperatura) { return 0; } public static double FahrenheitParaKelvin(double temperatura) { return 0; } } }

Na sequência deverá ser criado um projeto de testes chamado “ExemploBDD.Specs”, a partir da seleção do template “Unit Test Project” (Figura 8).

Figura 8. Criando o projeto ExemploBDD.Specs

Com o projeto ExemploBDD.Specs gerado, serão necessários alguns ajustes antes de seguir com a implementação das estruturas que farão parte do mesmo. O primeiro destes procedimentos consiste em apontar para a Class Library ExemploBDD, a qual foi definida anteriormente. Além disso, será preciso adicionar uma referência ao framework SpecFlow, o que acontecerá a partir do utilitário NuGet (Figura 9).

Figura 9. Adicionando o framework SpecFlow ao projeto de testes

Em seguida alterar a seção “specFlow” no arquivo app.config do projeto ExemploBDD.Specs, de forma que o mesmo contemple os seguintes ajustes (Listagem 2):

Listagem 2. Arquivo app.config do projeto ExemploBDD.Specs

<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow" /> </configSections> <specFlow> <language feature="pt-BR" /> <unitTestProvider name="MSTest" /> </specFlow> </configuration>

O próximo passo agora será a criação do arquivo “ConvTemperatura.feature”, representando a feature empregada na validação da funcionalidade de conversão de temperaturas. Essa especificação deverá ser gerada através do template “SpecFlow Feature File” (Figura 10), o qual foi habilitado durante o procedimento de instalação da extensão do SpecFlow.

Figura 10. Criando a especificação ConvTemperatura.feature

Embora o conteúdo inicial do arquivo ConvTemperatura.feature esteja em inglês, o Visual Studio já exibirá como opções palavras-chaves em português através do mecanismo de “code completion” disponibilizado pelo SpecFlow (Figura 11). Esta característica (suporte a um idioma específico) é possível graças ao mecanismo conhecido como Gherkin.

Figura 11. Code completion ao se utilizar o framework SpecFlow

Toda feature criada por meio do SpecFlow conta com um arquivo .cs correspondente (Figura 12). Neste último são geradas automaticamente uma série de instruções em .NET, as quais em conjunto determinam como os testes para a validação de uma funcionalidade serão executados (considerando para isto o framework de testes previamente configurado no app.config). Importante destacar que na grande maioria dos casos não haverá a necessidade de um desenvolvedor alterar a forma como o arquivo .cs foi gerado.

Figura 12. Arquivo .cs associado à especificação ConvTemperatura.feature

Os cenários que deverão ser levados em conta pelo arquivo ConvTemperatura.feature estão listados na Tabela 1. Por convenção, será assumido como regra geral o arredondamento para 2 casas decimais em todas as operações de conversão de temperaturas.

Temperaturas
Fahrenheit Celsius Kelvin
32 0 273,15
86 30 303,15
90,5 32,5 305,65
212 100 373,15

Tabela 1. Cenários previstos para a especificação ConvTemperatura.feature

Na Figura 13 é possível visualizar a user history utilizada na validação da funcionalidade de conversão de temperaturas.

Figura 13. User story para validação da funcionalidade de conversão de temperaturas

Analisando o arquivo ConvTemperatura.feature nota-se que o mesmo foi todo codificado em português. Termos como “Funcionalidade”, “Cenário”, “Dado”, “Quando”, “Então” e “E” iniciam todas as sentenças, além de estarem destacados em azul:

Será necessário agora implementar a estrutura que estabelece a conexão entre o tipo ConversorTemperatura e a user history definida no arquivo ConvTemperatura.feature. Para isto será criada dentro do projeto ExemploBDD.Specs uma nova classe chamada ConvTemperaturaStepDefinition, a qual se baseia no template “SpecFlow Step Definition” (Figura 14).

Figura 14. Criando uma classe baseada no template “SpecFlow Step Definition”

Em termos práticos, este novo arquivo conterá uma classe responsável por validar passo a passo (daí o nome “step”) os diferentes cenários especificados anteriormente. Na Figura 15 é possível observar parte do código gerado para a classe ConvTemperaturaStepDefinition.

Figura 15. Código gerado automaticamente para a classe ConvTemperaturaStepDefinition

O código que define a classe ConvTemperaturaStepDefinition está na Listagem 3.

Analisando a estrutura do tipo ConvTemperaturaStepDefinition, é possível notar o uso do atributo BindingAttribute, o qual faz parte do namespace TechTalk.SpecFlow. Isto é um pré-requisito para a correta execução de uma especificação com o SpecFlow, além da classe em questão ser declarada como pública.

O método marcado com o tipo GivenAttribute (namespace TechTalk.SpecFlow) efetua o mapeamento entre a classe ConvTemperaturaStepDefinition e as sentenças que começam com as palavras-chave “Dado”. Esta operação será invocada pelo engine do SpecFlow, a fim de carregar os parâmetros de entrada para posterior processamento:

Quanto ao atributo WhenAttribute (namespace TechTalk.SpecFlow), essa estrutura é responsável por associar uma sentença iniciada por “Quando” a um ou mais métodos responsáveis por realizar os processamentos esperados para uma funcionalidade. Os resultados destas ações serão analisados posteriormente, a fim de avaliar se o comportamento do recurso em questão está dentro do esperado. No caso do método ProcessarConversarTemperatura, foram invocadas as operações FahrenheitParaCelsius e FahrenheitParaKelvin da classe ConversorTemperatura: os retornos obtidos foram então associados a dois atributos privados (“_temperaturaCelsius” e “_temperaturaKelvin”).

Já os métodos vinculados ao atributo ThenAttribute (namespace TechTalk.SpecFlow) serão acionados pelo engine do SpecFlow durante o processamento de sentenças iniciadas pelas palavras-chave “Então” e “E”:

Listagem 3. Classe ConvTemperaturaStepDefinition

using TechTalk.SpecFlow; using Microsoft.VisualStudio.TestTools.UnitTesting; using ExemploBDD; namespace ExemploBDD.Specs { [Binding] public sealed class ConvTemperaturaStepDefinition { private double _temperaturaFahrenheit; private double _temperaturaCelsius; private double _temperaturaKelvin; [Given("que o valor da temperatura é de (.*) graus Fahrenheit")] public void CarregarTemperaturaFahrenheit(double temperatura) { this._temperaturaFahrenheit = temperatura; } [When("eu solicitar a conversão desta temperatura")] public void ProcessarConversaoTemperatura() { this._temperaturaCelsius = ConversorTemperatura .FahrenheitParaCelsius(this._temperaturaFahrenheit); this._temperaturaKelvin = ConversorTemperatura .FahrenheitParaKelvin(this._temperaturaFahrenheit); } [Then("o resultado da conversão para Celsius será de (.*) graus")] public void VerificarTemperaturaCelsius(double temperatura) { Assert.AreEqual(temperatura, this._temperaturaCelsius); } [Then("o resultado da conversão para Kelvin será de (.*) graus")] public void VerificarTemperaturaKelvin(double temperatura) { Assert.AreEqual(temperatura, this._temperaturaKelvin); } } }

OBSERVAÇÃO: por convenção, nomes de classes que representam atributos terminam com o sufixo Attribute. A utilização de expressões que envolvam um atributo vinculando o mesmo a uma estrutura de código dispensa o uso de tal sufixo ao final do nome. Logo, ao se empregar o atributo BindingAttribute, a classe marcada com essa construção estará associada apenas a uma instrução com o valor “Binding” (entre colchetes).

Terminada a implementação da classe ConvTemperaturaStepDefinition, chega agora o momento de processar os diferentes cenários para testes dos cálculos de conversão de temperaturas. Acessando o arquivo ConvTemperatura.feature com o menu de atalho, serão apresentadas opções (Figura 16) para a execução das instruções contidas nesta estrutura ou ainda, para a depuração dos cenários (com a execução passo-a-passo dos diferentes métodos declarados em ConvTemperaturaStepDefinition).

Figura 16. Opções para execução dos cenários do arquivo ConvTemperatura.feature

OBSERVAÇÃO: os cenários que constam em ConvTemperatura.feature também podem ser executados acessando no menu do Visual Studio o caminho Test > Run > All Tests.

Conforme esperado, a execução dos cenários contidos em ConvTemperatura.feature foi inconclusiva. Na Figura 17 está o resultado deste procedimento na janela “Test Explorer”, em que é possível visualizar a ocorrência de falhas em todos os casos de testes.

Figura 17. Cenários inconclusivos para o arquivo ConvTemperatura.feature

A próxima ação agora será a implementação dos métodos de conversão de temperatura, considerando para tanto as regras/fórmulas indicadas na Figura 18 em que: “C”, “F” e “K” correspondem, respetivamente, às temperaturas nas escalas Celsius, Fahrenheit e Kelvin.

Figura 18. Cálculos para conversão de temperaturas

Na Listagem 4 está a nova implementação da classe ConversorTemperatura, com os ajustes esperados para as funcionalidades de conversão de temperaturas.

Listagem 4. Classe ConversorTemperatura após a implementação dos cálculos

using System; namespace ExemploBDD { public static class ConversorTemperatura { public static double FahrenheitParaCelsius(double temperatura) { return Math.Round((temperatura - 32) / 1.8, 2); } public static double FahrenheitParaKelvin(double temperatura) { return Math.Round((temperatura - 32) / 1.8, 2) + 273.15; } } }

Uma nova execução da user story definida em ConvTemperatura.feature resultará, após os últimos ajustes, em sucesso em todos os cenários (Figura 19).

Figura 19. Cenários executados com sucesso

É possível ainda realizar uma pequena refatoração na classe ConversorTemperatura, como indicado na Listagem 5. A ideia com esta modificação foi evitar a repetição de código nos métodos FahrenheitParaCelsius e FahrenheitParaKelvin.

Listagem 5. Classe ConversorTemperatura após a implementação dos cálculos

using System; namespace ExemploBDD { public static class ConversorTemperatura { public static double FahrenheitParaCelsius(double temperatura) { return Math.Round((temperatura - 32) / 1.8, 2); } public static double FahrenheitParaKelvin(double temperatura) { return FahrenheitParaCelsius(temperatura) + 273.15; } } }

Uma última execução dos cenários especificados no arquivo ConvTemperatura.feature indicará que as alterações produziram o retorno esperado (Figura 20).

Figura 20. Cenários executados com sucesso após a refatoração

A intenção deste artigo foi apresentar a metodologia conhecida como BDD (Behavior-Driven Development), enfatizando ainda a importância em se estabelecer uma linguagem comum entre as áreas técnica e de negócios. A existência de um vocabulário claro dentro de um projeto é um ponto importantíssimo, já que não dará margem para interpretações incorretas e falhas de comunicação na definição de requisitos.

No caso específico de soluções construídas sob o .NET Framework no Visual Studio, o SpecFlow pode se revelar num instrumento de grande valia na adoção das práticas de BDD. Permitindo a execução de testes automatizados a partir de user stories, este framework permite conciliar ainda o trabalho de desenvolvimento com preceitos oriundos de metodologias ágeis como Scrum.

Espero que este conteúdo possa ter sido útil.

Até uma próxima oportunidade!

Links

Introducing BDD
http://dannorth.net/introducing-bdd/

SpecFlow
http://www.specflow.org/

Artigos relacionados