Pontos de dados

Design controlado por comportamento com o SpecFlow

Julie Lerman

Baixar o código de exemplo

Julie LermanA essa altura, você já está familiarizado com minha predileção em convidar desenvolvedores para falar a respeito de tópicos sobre os quais tenho curiosidade no grupo de usuários que lidero em Vermont. O resultado foram colunas envolvendo tópicos como Knockout.js e Breeze.js. Há ainda muitos outros tópicos que venho abordando por algum tempo, como o CQRS (Command Query Responsibility Segregation). Entretanto, Dennis Doire, um arquiteto e testador, falou recentemente sobre o SpecFlow e o Selenium, duas ferramentas usadas por testadores no desenvolvimento controlado por comportamento (BDD). Novamente, meus olhos saltaram e minha mente começou a buscar desculpas para experimentar as ferramentas. No entanto, foi o BDD quem na verdade atraiu minha atenção. Apesar de ser uma pessoa orientada por dados, meus dias como designer de aplicativos, de bancos de dados a soluções mais avançadas, ficaram para trás, pois o meu interesse passou a se concentrar no domínio.

O BDD representa uma nova perspectiva no desenvolvimento orientado a testes (TDD); seu foco concentra-se nas histórias de usuários e na criação de uma lógica e testes em torno dessas histórias. Em vez de satisfazer apenas certa regra única, conjuntos de atividades são atendidos. É algo bastante holístico que muito me agrada, uma perspectiva do meu extremo interesse. A ideia é a seguinte: enquanto uma unidade de teste típica garante o correto funcionamento de eventos individuais no objeto do cliente, o BDD se concentra no histórico mais amplo do comportamento que eu, a usuária, espero obter ao usar o sistema que você está desenvolvendo para mim. Geralmente, o BDD é usado na definição de critérios de aceitação durante discussões com clientes. Por exemplo, quando me sento em frente ao computador e preencho o cadastro de um novo cliente e pressiono o botão Salvar, o sistema deverá armazenar as informações do cliente e mostrar-me uma mensagem de que o cliente foi registrado com sucesso.

Ou talvez ao ativar a seção de Gerenciamento de clientes do software, o aplicativo deverá abrir automaticamente o Cliente mais recente com o qual trabalhei em minha última sessão.

Essas histórias de usuários nos mostram que o BDD pode ser uma técnica orientada por interface do usuário para elaborar testes automatizados, porém muitos dos cenários são estabelecidos antes da criação da UI. E graças a ferramentas como o Selenium (docs.seleniumhq.org) e WatiN (watin.org), é possível automatizar testes no navegador. No entanto, o BDD não se limita a simplesmente descrever interações do usuário. Para obter uma visão mais ampla do BDD, confira o painel de discussão sobre InfoQ envolvendo algumas das autoridades em BDD, TDD e Especificação pelo exemplo em bit.ly/10jp6ve.

Não pretendo concentrar minha preocupação no clicar de botões e coisas do tipo, mas redefinir um pouco mais as histórias de usuários. Posso remover da história os elementos dependentes da interface do usuário e concentrar-me na porção do processo que independe da tela. Naturalmente, estou interessada nas histórias relacionadas a acesso a dados.

Criar a lógica a ser usada para testar se um comportamento em particular está sendo satisfeito pode ser algo cansativo. Uma das ferramentas demonstradas por Doire em sua apresentação foi o SpecFlow (specflow.org). A ferramenta se integra ao Visual Studio e permite definir histórias de usuários, conhecidas como cenários, utilizando suas regras simples. A partir daí, a ferramenta automatiza parte da criação e execução dos métodos (alguns com testes, e outros, sem). O objetivo é validar se as regras da história estão sendo satisfeitas.

Irei orientá-lo a criar alguns comportamentos para aguçar o seu apetite e, caso esteja interessado em mais informações, você encontrará um bom número de recursos ao final do artigo.

Primeiramente, será necessário instalar o SpecFlow no Visual Studio; esse procedimento pode ser executado nas Extensões do Visual Studio e no Gerenciador de Atualizações. Visto que o objetivo do BDD é iniciar o desenvolvimento de projetos por meio de uma descrição de comportamentos, o primeiro projeto da sua solução será um projeto de teste, no qual você irá descrever os referidos comportamentos. O restante da solução seguirá desse ponto em diante.

Crie um novo projeto usando o modelo de projeto de teste unitário. O projeto irá requerer uma referência a TechTalk.SpecFlow.dll, que pode ser instalada utilizando NuGet. Em seguida, crie uma pasta chamada Recursos dentro do projeto.

Meu primeiro recurso se baseará em uma história de usuário sobre como adicionar novos clientes; sendo assim, na pasta Recursos, criei outra pasta chamada Adicionar (veja a Figura 1). Neste ponto, irei definir meu cenário e solicitar ajuda do SpecFlow.

Test Project with Features and Add Sub-Folders
Figura 1 Projeto de teste com recursos e subpastas Adicionar

O SpecFlow segue um padrão específico dependente de palavras-chave que ajudam a descrever o recurso cujo comportamento você está definindo. As palavras-chave vêm de uma linguagem chamada Gherkin (pepino em conserva; sim, como em picles) e tudo isso se origina de uma ferramenta chamada Cucumber, isto é, pepino (cukes.info). Algumas dessas palavras-chave são Dado, E, Quando e Então; use-as na criação de um cenário. Por exemplo, veja este cenário simples, encapsulado em determinado recurso — Adicionando um novo cliente:

Given a user has entered information about a customer
When she completes entering more information
Then that customer should be stored in the system

É possível elaborar um pouco mais; por exemplo:

Given a user has entered information about a customer
And she has provided a first name and a last name as required
When she completes entering more information
Then that customer should be stored in the system

A última instrução representa o ponto onde executarei certa persistência de dados. O SpecFlow não se preocupa com a forma como esses processos acontecem. O objetivo é compilar cenários a fim de provar que o resultado é e permanece bem-sucedido. O cenário direcionará o conjunto de testes, e os testes o ajudarão a definir sua lógica do domínio:

Given that you have used the proper keywords
When you trigger SpecFlow
Then a set of steps will be generated for you to populate with code
And a class file will be generated that will automate the execution of these steps on your behalf

Vejamos como isso funciona.

Clique com o botão direito do mouse na pasta Adicionar para adicionar um novo item. Se o SpecFlow tiver sido instalado, é possível encontrar três itens relacionados ao aplicativo pesquisando no specflow. Selecione o item do arquivo Recurso do SpecFlow e nomeie-o. O meu chama-se AddCustomer.feature.

Arquivos de recursos começam com um exemplo, a característica mais comum da matemática. Observe que o Recurso é descrito na parte superior; na parte inferior, temos a descrição de um Cenário (que representa um exemplo-chave do recurso) utilizando Dado, E, Quando e Então. O complemento SpecFlow garante a codificação do texto por cores; dessa forma, é possível distinguir facilmente entre os termos da etapa e suas próprias instruções:

Substituirei o recurso e as etapas predefinidos pelos meus:

Feature: Add Customer
Allow users to create and store new customers
As long as the new customers have a first and last name

Scenario: HappyPath
Given a user has entered information about a customer
And she has provided a first name and a last name as required
When she completes entering more information
Then that customer should be stored in the system

(agradeço a David Starr pelo nome do Cenário! Surripiei-o do seu vídeo Pluralsight).

E se os dados necessários não forem fornecidos? Criarei outro cenário neste recurso para lidar com a possibilidade:

Scenario: Missing Required Data
Given a user has entered information about a customer
And she has not provided the first name and last name
When she completes entering more information
Then that user will be notified about the missing data
And the customer will not be stored into the system

Isso irá funcionar por ora.

De história do usuário para algum código

Até aqui, vimos o item Recursos e a codificação por cores encontrados no SpecFlow. Observe o arquivo codebehind anexado ao arquivo de recursos, no qual há alguns testes vazios criados com base nesses recursos. Cada um desses testes executará as etapas do cenário, mas será necessário criar as referidas etapas. Há algumas formas de fazer isso. Você poderá executar os testes, e o SpecFlow retornará a listagem de códigos referentes à classe Steps no resultado do teste para serem copiados e colados. Como opção, é possível usar uma ferramenta no menu de contexto do arquivo de recursos. Veja a seguir a descrição da segunda abordagem:

  1. Clique com o botão direito do mouse na janela do editor de texto do arquivo de recursos. No menu de contexto, há uma seleção dedicada a tarefas do SpecFlow.
  2. Clique em Gerar definições da etapa. Será exibida uma janela verificando as etapas a serem criadas.
  3. Clique no botão Copiar métodos na área de transferência e use os padrões.
  4. Na pasta AddCustomer do projeto, crie um novo arquivo de classe com o nome Steps.cs.
  5. Abra o arquivo e, na definição de classe, copie o conteúdo da área de transferência.
  6. Adicione uma referência do namespace ao início do arquivo utilizando TechTalk.SpecFlow.
  7. Adicione à classe uma anotação de Vinculação.

A nova classe é listada na Figura 2.

Figura 2 O arquivo Steps.cs

[Binding]
public class Steps
{
  [Given(@"a user has entered information about a customer")]
  public void GivenAUserHasEnteredInformationAboutACustomer()
  {
    ScenarioContext.Current.Pending();
  }
  [Given(@"she has provided a first name and a last name as required")]
  public void GivenSheHasProvidedAFirstNameAndALastNameAsRequired
 ()
  {
    ScenarioContext.Current.Pending();
  }
    [When(@"she completes entering more information")]
  public void WhenSheCompletesEnteringMoreInformation()
  {
    ScenarioContext.Current.Pending();
  }
  [Then(@"that customer should be stored in the system")]
  public void ThenThatCustomerShouldBeStoredInTheSystem()
  {
    ScenarioContext.Current.Pending();
  }
  [Given(@"she has not provided both the firstname and lastname")]
  public void GivenSheHasNotProvidedBothTheFirstnameAndLastname()
  {
    ScenarioContext.Current.Pending();
  }
  [Then(@"that user will get a message")]
  public void ThenThatUserWillGetAMessage()
  {
    ScenarioContext.Current.Pending();
  }
  [Then(@"the customer will not be stored into the system")]
  public void ThenTheCustomerWillNotBeStoredIntoTheSystem()
  {
    ScenarioContext.Current.Pending();
  }
}

Observe os meus dois cenários criados; você notará que embora haja uma sobreposição nos elementos definidos (como por exemplo, “um usuário inseriu informações sobre determinado cliente”), os métodos gerados não criam etapas em duplicidade. Vale a pena observar também que o SpecFlow aproveita as constantes dos atributos do método. Os nomes reais do método são irrelevantes.

Neste ponto, você poderá deixar o SpecFlow executar os testes que acionam esses métodos. Embora o SpecFlow seja compatível com certas estruturas para testes de unidades, utilizei o MSTest; por isso, se estiver analisando a solução no Visual Studio, você observará que o arquivo codebehind do Recurso define um TestMethod para cada cenário. Cada TestMethod executa a correta combinação de métodos por etapa utilizando um TestMethod executado no cenário HappyPath.

Se eu fosse executar o procedimento agora, clicando com o botão direito do mouse no arquivo Recurso e escolhendo “Executar cenários do SpecFlow”, o teste seria inconclusivo, com a seguinte mensagem: “Uma ou mais definições de etapa não foram ainda implementadas”. O motivo é que todos os métodos do arquivo Etapas ainda estão chamando o parâmetro Scenario.Current.Pending.

Portanto, é hora de definir os métodos. Meus cenários informam que precisarei de um tipo de Cliente com certos dados necessários. Graças a outras documentações, sei que o primeiro e o último nome são obrigatórios no momento; dessa forma, essas duas propriedades são necessárias no tipo de Cliente. Preciso também de um mecanismo para armazenar o cliente, bem como de um local. Meus testes não se preocupam com a forma e o local de armazenamento, apenas com sua natureza; sendo assim, utilizarei um repositório cuja responsabilidade será a de obter e armazenar dados.

Começarei adicionando as variáveis _customer e _repository à classe Steps:

private Customer _customer;
private Repository _repository;

Em seguida, criarei uma classe Customer:

public class Customer
{
  public int Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

Isso é o suficiente para poder adicionar códigos aos meus métodos por etapa. A Figura 3 mostra a lógica adicionada às etapas relacionadas ao HappyPath. Criei um novo cliente em um deles e, em seguida, forneci o primeiro e último nomes no seguinte. Na verdade, não preciso de mais nada para poder elaborar na etapa WhenSheCompletesEnteringMoreInformation.

Figura 3 Alguns dos métodos do SpecFlow Step

[Given(@"a user has entered information about a customer")]
public void GivenAUserHasEnteredInformationAboutACustomer()
{
  _newCustomer = new Customer();
}
[Given(@"she has provided a first name and a last name as required")]
public void GivenSheHasProvidedTheRequiredData()
{
  _newCustomer.FirstName = "Julie";
  _newCustomer.LastName = "Lerman";
}
[When(@"she completes entering more information")]
public void WhenSheCompletesEnteringMoreInformation()
{
}

A última etapa é a mais interessante: É a etapa em que não apenas armazenei o cliente, mas provei que ele de fato foi armazenado. Precisarei de um método Adicionar em meu repositório para armazenar o cliente, uma opção Salvar para adicioná-lo ao banco de dados e, por fim, uma forma de verificar se o cliente pode ser, de fato, localizado no repositório. Portanto, incluirei um método Adicionar, uma opção Salvar e um método FindById ao repositório, como o seguinte:

public class CustomerRepository
{
  public void Add(Customer customer)
    { throw new NotImplementedException();  }
  public int Save()
    { throw new NotImplementedException();  }
  public Customer FindById(int id)
    { throw new NotImplementedException();  }
}

Agora, posso adicionar lógica à etapa final e esta será acionada por meu cenário HappyPath. Adicionarei o cliente ao repositório e testarei se é possível localizá-lo ali. É nesse ponto onde finalmente utilizo uma asserção para determinar se meu cenário está obtendo êxito. Se o cliente for encontrado (isto é, IsNotNull), o teste foi aprovado. Temos aqui um padrão bastante comum para testar se os dados foram armazenados. Contudo, com base em minha experiência no Entity Framework, vejo um problema geralmente não revelado pelo teste. Começarei com o código abaixo e assim poderei demonstrar o problema de uma forma mais fácil de memorizar do que simplesmente mostrar o jeito certo de começar (isto é, aquele jeito):

[Then(@"that customer should be stored in the system")]
public void ThenThatCustomerShouldBeStoredInTheSystem()
{
  _repository = new CustomerRepository();
  _repository.Add(_newCustomer);
  _repository.Save();
  Assert.IsNotNull(_repository.FindById(_newCustomer.Id));
}

Ao executar novamente meu HappyPath, o teste é reprovado. Veja na Figura 4 que o resultado do teste mostra como meu cenário SpecFlow está funcionando até aqui. Mas preste atenção ao motivo pelo qual o teste foi reprovado: Não se trata do fato de o FindById ter falhado em encontrar o cliente; o problema é que meus métodos de repositório ainda não foram implementados.

Figura 4 Resultado do teste reprovado apresentando o status de cada etapa

Test Name:  HappyPath
Test Outcome:               Failed
Result Message:             
Test method UnitTestProject1.UserStories.Add.AddCustomerFeature.HappyPath threw exception:
System.NotImplementedException: The method or operation is not implemented.
Result StandardOutput:     
Given a user has entered information about a customer
-> done: Steps.GivenAUserHasEnteredInformationAboutACustomer() (0.0s)
And she has provided a first name and a last name as required
-> done: Steps. GivenSheHasProvidedAFirstNameAndALastNameAsRequired() (0.0s)
When she completes entering more information
-> done: Steps.WhenSheCompletesEnteringMoreInformation() (0.0s)
Then that customer should be stored in the system
-> error: The method or operation is not implemented.

Portanto, a próxima etapa é adicionar lógica ao meu repositório. Por fim, utilizarei esse repositório como forma de interagir com o banco de dados e, por ser fã do Entity Framework, utilizarei um DbContext do Entity Framework em meu repositório. Começarei criando uma classe DbContext que expõe um DbSet Customers:

public class CustomerContext:DbContext
{
  public DbSet<Customer> Customers { get; set; }
}

Assim, posso refatorar meu CustomerRepository de modo a usar o CustomerContext para fins de persistência. Nesta demonstração, trabalharei diretamente no contexto, sem me preocupar com abstrações. Este é o CustomerRepository atualizado:

public  class CustomerRepository
{
  private CustomerContext _context = new CustomerContext();
  public void Add(Customer customer
  {    _context.Customers.Add(customer);  }
  public int Save()
  {    return _context.SaveChanges();  }
  public Customer FindById(int id)
  {    return _context.Customers.Find(id);  }
}

Em seguida, ao executar novamente o HappyPath, o teste é aprovado e todas as etapas são assinaladas como concluídas. Mas ainda não estou satisfeita.

Certifique-se de que os testes de integração compreendem o comportamento do EF

Por que ainda não estou satisfeita com a aprovação dos meus testes e o círculo bem verde? Porque sei que o teste não está provando de fato o armazenamento do cliente.

No método ThenThatCustomerShouldBeStoredInTheSystem, confirme a instrução para Salvar e executar o teste novamente. O teste ainda é aprovado. E eu nem mesmo salvei o cliente no banco de dados! Não sente um cheiro de desconfiança? Aquele famoso cheiro conhecido como “falso positivo”.

O problema está no método DbSet Find que estou usando em meu repositório, pois trata-se de um método especial do Entity Framework; seu trabalho é primeiramente verificar os objetos na memória que estão sendo rastreados pelo contexto antes de passar para o banco de dados. Quando chamei Adicionar, deixei o CustomerContext informado sobre essa instância do cliente. A chamada a Customer.Find descobriu a instância e ignorou uma viagem perdida ao banco de dados. Na verdade, o ID do cliente permanece como 0, pois ainda não foi armazenado.

Portanto, pelo fato de estar usando o Entity Framework (leve em consideração o comportamento de qualquer estrutura de mapeamento relacional de objetos [ORM] em uso), disponho de um meio mais simples de testar e verificar se o cliente realmente foi armazenado no banco de dados. Ao inserir o cliente no banco de dados, a instrução SaveChanges do EF extrai o novo ID do cliente gerado pelo banco de dados e o aplica à instância inserida. Consequentemente, se o novo ID não for mais 0, sei que realmente o cliente foi inserido no banco de dados. Não preciso consultar outra vez o banco de dados.

Farei a devida reavaliação da Assertiva em relação a esse método. Veja a seguir o método que certamente executará um teste apropriado:

[Then(@"that customer should be stored in the system")]
  public void ThenThatCustomerShouldBeStoredInTheSystem()
  {
    _repository = new CustomerRepository();
    _repository.Add(_newCustomer);
    _repository.Save();
    Assert.IsNotNull(_newCustomer.Id>0);
  }

O teste é aprovado e sei que pelos motivos certos. Não é incomum definir testes reprovados, por exemplo, usando Assert.IsNull(FindById(customer.Id) como meio de garantir que a aprovação não se deu pelo motivo errado. Nesse caso, porém, o problema permaneceria encoberto se eu não removesse o chamado para Salvar. Se não estiver seguro quanto à forma como o EF funciona, seria aconselhável criar alguns testes de integração específicos, não relacionados às histórias de usuários, a fim de garantir que os repositórios estão se comportando como esperado.

Teste de comportamento ou de integração?

Ao transpor a curva do aprendizado de como elaborar este primeiro cenário do SpecFlow, me deparei com o que julguei ser uma situação perigosa. Segundo o meu cenário, o cliente deveria estar armazenado “no sistema”.

O problema é que não me sentia segura quanto à definição do sistema. Minha experiência diz que o banco de dados, ou pelo menos algum mecanismo de persistência, é uma parte extremamente importante do sistema.

A usuária não está preocupada com repositórios e bancos de dados; apenas com seus aplicativos. Contudo, ela não se sentirá nenhum pouco satisfeita se acessar novamente o aplicativo e não puder localizar o cliente pelo fato de ele nunca ter sido armazenado no banco de dados (pois não pensei que _repository.Save fosse necessário para completar o cenário).

Consultei-me com outro Dennis, Dennis Doomen, autor de Fluent Assertions e um profissional bastante requisitado para a implementação de BDD, TDD e sistemas empresariais de grande porte. Como desenvolvedor, ele confirmou que eu certamente deverei aplicar meu conhecimento às etapas e testes, mesmo se isso for além da intenção do usuário autor da definição do cenário original. Os usuários apresentam seu conhecimento e eu acrescento o meu, sem forçá-los a aceitar minha perspectiva técnica. Continuei falando na linguagem da usuária e mantendo uma boa comunicação com ela.

Explore cada vez mais o BDD e o SpecFlow

Tenho plena certeza de que se não fosse pelas ferramentas desenvolvidas para dar suporte ao BDD, meu trabalho com o recurso não teria sido tão fácil. Mesmo sendo fissurada em dados, me preocupo bastante com o trabalho junto aos clientes, procurando compreender seu ramo de negócios e garantindo a eles uma grata experiência no uso do software que os ajudei a desenvolver. Por essa razão, Design controlado por domínio e Design controlado por comportamento são muito importantes para mim. Creio ser esse o sentimento de muitos desenvolvedores, mesmo se estiver enraizado em suas almas (ou corações), que também se veem inspirados por essas técnicas.

Além dos amigos que me ajudaram a chegar até aqui, veja a seguir alguns dos recursos que considero úteis. O artigo da MSDN Magazine, “Behavior-Driven Development with SpecFlow and WatiN,” que pode ser acessado em msdn.microsoft.com/magazine/gg490346, foi extremamente útil. Assisti também a um excelente módulo do curso Test First Development (Teste o primeiro desenvolvimento) de David Starr em Pluralsight.com. (Na verdade, assisti ao módulo uma infinidade de vezes). Achei interessante a entrada na Wikipedia sobre BDD (bit.ly/LCgkxf); o artigo apresenta uma visão mais ampla da história do BDD e como ele se adapta a outras práticas. Estou aguardando ansiosa o livro “BDD and Cucumber”, com Paul Rayner (que também me aconselhou aqui) como coautor.

Julie Lerman é uma Microsoft MVP, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrá-la fazendo apresentações sobre acesso a dados e outros tópicos do Microsoft .NET em grupos de usuários e conferências em todo o mundo. Seu blog está em thedatafarm.com/blog e ela é autora do livro “Programming Entity Framework” (2010), além das edições Code First (2011) e DbContext (2012), todos da O’Reilly Media. Siga-a no Twitter, em twitter.com/julielerman.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Dennis Doomen (Aviva Solutions) e Paul Rayner (Virtual Genius)
Dennis Doomen é consultor principal na Aviva Solutions (Holanda), palestrante ocasional, instrutor versátil, autor das Diretrizes de codificação para C# 3.0, 4.0 e 5.0, da estrutura Fluent Assertions e do Silverlight Cookbook. Atualmente, ele desenvolve soluções de classe empresarial baseadas em .NET, Event Sourcing e CQRS. Dennis é amante do desenvolvimento ágil, arquitetura, programação extrema e design controlado por domínio. Você pode contatá-lo pelo Twitter usando @ddoomen.