Eu tenho falado bastante sobre Behavior Driven Development (BDD), e explicado geralmente usando Ruby com Cucumber e Selenium testando código .NET. Muitos me pediam um exemplo em .NET, e esse post é um apontamento nesta direção. Estou realizando uma consultoria nesse momento onde ensino estes conceitos, e lembrei que estava devendo este post. Não vai ser uma longa explicação sobre BDD, mas só uma forma de como fazer BDD direito, com outside in, sem sair do .NET.

Nem vou gastar muito tempo explicando pra vocês que BDD é muito legal, e que pode te ajudar a tornar o teu projeto mais previsível, além de te deixar mais feliz. Você já sabe disso. Vamos ao concreto.

Um tempo atrás eu mostrei o StoryQ para ajudar a escrever especificações com .NET. Desta vez vou usar outra ferramenta, o SpecFlow, que se aproxima bem mais do modelo do Cucumber. A versão atual está se integrando muito bem ao Visual Studio, e ficou bem fácil trabalhar, bem diferente das primeiras versões que usei no começo do projeto. Pra facilitar, como tudo atualmente, ele também está no Nuget.

E para automatizar o navegador eu vou mudar também. Vamos com Watin. Funciona bem, vale a pena conferir e também está no Nuget.

Pra começar, criei uma aplicação ASP.NET MVC 3, e mandei ele criar o projeto de testes, com MSTest mesmo, não mudei nada. Em seguida, instalei o Specflow e o Watin via Nuget.

Instalando Specflow com Nuget

Só pra constar: Atualizei meu jQuery, também via Nuget (viva a versão 3!):

Com isso, já posso começar a escrever a especificação. Vou fazer uma aplicação que calcula o imposto de renda. Pra quem não quiser ficar triste e conferir no site da Receita Federal, ela funciona assim:

 

Base de cálculo mensal em R$ Alíquota % Parcela a deduzir do imposto em R$
Até 1.499,15
De 1.499,16 até 2.246,75 7,5 112,43
De 2.246,76 até 2.995,70 15,0 280,94
De 2.995,71 até 3.743,19 22,5 505,62
Acima de 3.743,19 27,5 692,78

Como o objetivo é trabalhar de fora pra dentro, começo pela história. Vou escrevê-la em um arquivo de funcionalidade, ou .feature:

Feature file no SpecFlow

O SpecFlow suporta o Gherkin, que é a sintaxe usada também no Cucumber. E também suporta português. Então basta informar a língua no começo do arquivo que ele passa a funcionar na nossa bela língua pátria. E com Intelisense e syntax highlighting:

Escrevendo a feature com SpecFlow no VS

Aí já escrevi o cenário também. Só pra repaassar, essa foi minha feature:

Funcionalidade: Cálculo do valor do IR
	Para quanto vou pagar de imposto
	Enquanto contribuinte
	Eu gostaria de calcular o IR

Cenário: Salário de 100 reais fica isento
	Dado que estou na página IR
	E preencho o campo 'salario' com o valor 100.00
	Quando clico em Calcular
	Então vejo 'O resultado é 0.00'

Compila, dá erro, porque o padrão do SpecFlow é usar NUnit. Basta mudar a configuração no app.config:

<specFlow>
  <unitTestProvider name="MSTest" />
</specFlow>

 

E resolvido. O Specflow, debaixo do arquivo .feature, cria um .feature.cs, que é onde está o teste de verdade:

Arquivo .cs do SpecFlow no VS

Nem olhe esse arquivo. Imagine que o que roda mesmo é o arquivo da feature.

Agora vamos rodar. O resultado é esse:

Resultado da rodada de testes

Inconclusivo. Claro. Como o SpecFlow saberia como rodar esse teste? Aí é que começa a parte divertida. Ele nos entrega um código que podemos usar para implementar os passos que executarão cada frase do arquivo de funcionalidades. Basta acrescentar um arquivo de passos:

Step definition no SpecFlow

Então é só copiar e colar neste arquivo o resultado do teste. Ficará assim:

[Binding]
public class IRSteps
{
    [Given(@"preencho o campo 'salario' com o valor 100\.00")]
    public void DadoPreenchoOCampoSalarioComOValor100_00()
    {
        ScenarioContext.Current.Pending();
    }

    [Given(@"que estou na página IR")]
    public void DadoQueEstouNaPaginaIR()
    {
        ScenarioContext.Current.Pending();
    }

    [Then(@"vejo 'O resultado é 0\.00'")]
    public void EntaoVejoOResultadoE0_00()
    {
        ScenarioContext.Current.Pending();
    }

    [When(@"clico em Calcular")]
    public void QuandoClicoEmCalcular()
    {
        ScenarioContext.Current.Pending();
    }
}

 

Agora temos que implementar o trabalho real é aí que entra o Watin. Precisamos navegar, chamar o Internet Explorer (ou Firefox), preencher campos, clicar em botões, e avaliar resultados. Isso é fácil, vejam como ficou a classe de steps:

[Binding]
public class IRSteps 
{
    private IE _browser;

    [Given(@"preencho o campo 'salario' com o valor 100\.00")]
    public void DadoPreenchoOCampoSalarioComOValor100_00()
    {
        _browser.TextField(Find.ByName("salario")).TypeText("100.00");
    }

    [Given(@"que estou na página IR")]
    public void DadoQueEstouNaPaginaIR()
    {
        _browser = new IE("http://localhost:51000/IR");
    }

    [Then(@"vejo 'O resultado é 0\.00'")]
    public void EntaoVejoOResultadoE0_00()
    {
        _browser.ContainsText("O resultado é 0.00").Should().BeTrue();
    }

    [When(@"clico em Calcular")]
    public void QuandoClicoEmCalcular()
    {
        _browser.Button(Find.ByValue("Calcular")).Click();
    }

    [AfterScenario]
    public void FechaBrowser()
    {
        _browser.Dispose();
    }
}

Ao rodar a espeficação vai falhar, claro, já que a página /IR não existe.

Fique atento com esse erro ao rodar os testes:

System.IO.FileNotFoundException: Could not load file or assembly ‘Interop.SHDocVw, Version=1.1.0.0, Culture=neutral, PublicKeyToken=db7cfd3acb5ad44e’ or one of its dependencies. The system cannot find the file specified.

Ao compilar você verá um warning avisando deste arquivo e de mais outro, o “Microsoft.mshtml.dll”. Ambos não devem estar mesclados no assembly de teste. Marque “Embed Interop Types” como False para ambos os casos.

A partir daí, basta implementar, ou seja, criar o controller, e as classes de domínio. Aqui é onde você deveria criar outras especificações pra trabalhar só a entidade de domínio, ou com TDD trabalhar o controller. Vou pular esta parte. O controller fica assim:

public class IRController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public ActionResult Index(decimal salario)
    {
        ViewBag.Mensagem = "O resultado é 0.00";
        return View((object)salario);
    }
}

A partir daí a especificação vai passar. Rode o projeto de testes e verá o IE abrir, preencher os valores, e clicar no botão, além do assert que acontecerá depois.

No próximo post eu vou refatorar essa especificação pra passarmos a reaproveitar o trabalho realizado na spróximas especificações. É a partir daí que ficamos verdadeiramente produtivos.