Clique aqui para ler esse artigo em PDF.
Projeto/Análise - Expert
Testes Unitários
Mocks e Stubs – Parte 1
Neste artigo veremos |
·Testes unitários; ·Mocks e Stubs; ·.Net 3.5, Visual Studio 2008 e VB 9.0; ·Rhino Mocks. |
Qual a finalidade |
·Explicar o conceito de testes unitários com mocks e stubs. |
Quais situações utilizam esses recursos? |
·Criação de testes unitários para quaisquer projetos que envolvam uma boa separação de responsabilidades. |
Resumo do DevMan
Testes unitários são importantes para garantir a qualidade do código. Neste artigo, aprenda a fazer testes unitários com Mocks e Stubs, e entre em contato com conceitos de OO como injeção de dependência e separação de responsabilidades.
Falar de testes unitários é sempre interessante e tem ficado ainda mais interessante recentemente. A comunidade de desenvolvimento e as empresas têm se esforçado para criar soluções melhores, e novas abordagens têm surgido a cada dia.
Quando falamos de testes unitários logo vêm à mente termos como TDD, BDD, Stubs, Mocks, entre diversos outros. Ferramentas também surgem freqüentemente para apoiar todos esses nomes e siglas.
Neste artigo veremos porque usar testes unitários, como criar uma aplicação testável, e como utilizar ferramentas que nos auxiliem nos testes. Começaremos com uma aplicação simples, do tipo que vemos todos os dias, e vamos buscar entender porque a abordagem mais comum no desenvolvimento de sistemas traz problemas graves na hora de aplicarmos testes. Veremos também como alguns padrões de projeto (Design Patterns) vão nos ajudar a criar aplicações mais testáveis (falamos de Design Patterns em uma série de cinco artigos que foi da edição 47 à 51).
Aplicação de exemplo
Vamos montar uma aplicação de exemplo para podermos apoiar os futuros testes. A aplicação será extremamente simples. Para que possamos focar na solução de testes unitários, utilizaremos um banco de dados de exemplo da Microsoft para SQL Server 2005 e 2008 chamado AdventureWorks, disponível via Web no Codeplex no endereço http://www.codeplex.com/MSFTDBProdSamples/Release/ProjectReleases.aspx?ReleaseId=4004 . A aplicação estará separada em 6 camadas, sendo 4 projetos, gerando por isso 4 dlls, uma destas camadas sendo a de apresentação, nesse caso Web. Hoje esse tipo de interface é a mais comum (e portanto mais conhecida de todos), mas poderíamos utilizar qualquer outra.
O projeto tratará de exibir dados das tabelas SalesOrderHeader e Contact do banco AdventureWorks. A exibição será feita em um único formulário Web, conforme a Figura 1. Neste formulário, o grid exibe as colunas Order ID, Order Date e Total Due que têm a origem dos dados na tabela SalesOrderHeader, em colunas de nome semelhante, e a coluna Customer, que vêm das colunas Contact.FirstName e Contact.LastName concatenadas. Será possível paginar os dados com um engine customizado, e ir a uma página específica. O código desta página (default.aspx) e do code behind (default.aspx.vb) estão na Listagem 1. Este código serve apenas para que possamos montar a aplicação, mas não será importante para o objetivo do artigo, servindo apenas para exemplificar a interação da camada de apresentação com as outras camadas (procure a palavra-chave new e encontrará esses relacionamentos). Note que os únicos métodos de negócio chamados são os métodos GetOrders e GetQuantityOfOrders. O código da default.aspx contém somente as partes importantes, como o gridview e os outros controles, e o exemplo segue o inglês, já que os dados e os elementos do banco também estão nesta língua.
Figura 1. Resultado final da solução
Listagem 1. Arquivos default.aspx e default.aspx.vb
<asp:GridView ID="gvOrders" runat="server" AutoGenerateColumns="False">
<Columns>
<asp:BoundField DataField="SalesOrderID" HeaderText="Order ID" />
<asp:BoundField DataField="OrderDate" HeaderText="Order Date"
DataFormatString="{0:d}" />
<asp:TemplateField HeaderText="Customer">
<ItemTemplate>
<asp:Label ID="Label1" runat="server" Text='<%# Eval("Contact.FirstName") & " " & Eval("Contact.LastName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="TotalDue" HeaderText="Total Due"
DataFormatString="{0:F}" />
</Columns>
</asp:GridView>
<br />
<asp:LinkButton ID="lnkPrevious" runat="server"><</asp:LinkButton>
<asp:LinkButton ID="lnkNext" runat="server">></asp:LinkButton>
<asp:LinkButton ID="lnkbutGoToPage" runat="server">Go to page:</asp:LinkButton>
<asp:TextBox ID="txtGoToPage" runat="server" MaxLength="5" Width="38px">1</asp:TextBox>
<asp:RequiredFieldValidator ID="rvGoToPage" runat="server"
ControlToValidate="txtGoToPage" Display="Dynamic"
ErrorMessage="Insert a Number">*</asp:RequiredFieldValidator>
<asp:CompareValidator ID="cvGoToPage" runat="server"
ControlToValidate="txtGoToPage" Display="Dynamic"
ErrorMessage="Insert a valid number" Operator="DataTypeCheck">*</asp:CompareValidator>
<br />
<br />
Pages:
<asp:Label ID="lblCurrentPage" runat="server" Text="undefined"></asp:Label>
/<asp:Label ID="lblPages" runat="server" Text="undefined"></asp:Label>
<asp:ValidationSummary ID="ValidationSummary1" runat="server" />
Partial Public Class _Default
Inherits System.Web.UI.Page
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Not Page.IsPostBack Then
Dim bsOrders As New Business.Order()
Dim intQtty = bsOrders.GetQuantityOfOrders()
PagesQuantity = CInt(Math.Ceiling(intQtty / PageSize))
LoadGrid()
End If
End Sub
Property PagesQuantity() As Integer
Get
Return DirectCast(ViewState("PagesQuantity"), Integer)
End Get
Set(ByVal value As Integer)
ViewState("PagesQuantity") = value
lblPages.Text = Me.PagesQuantity.ToString()
End Set
End Property
Private Sub LoadGrid()
Dim bsOrders As New Business.Order()
Dim orders = bsOrders.GetOrders(CurrentPage, PageSize)
gvOrders.DataSource = orders
gvOrders.DataBind()
bsOrders.GetQuantityOfOrders()
End Sub
Private Const PageSize As Integer = 10
Public Property CurrentPage() As Integer
Get
If ViewState("CurrentPage") Is Nothing Then
Me.CurrentPage = 0
End If
Return DirectCast(ViewState("CurrentPage"), Integer)
End Get
Set(ByVal value As Integer)
If value >= 0 Then
If value >= PagesQuantity - 1 Then
ViewState("CurrentPage") = PagesQuantity - 1
Else
ViewState("CurrentPage") = value
End If
Else
ViewState("CurrentPage") = 0
End If
lblCurrentPage.Text = CStr(CurrentPage + 1)
End Set
End Property
Protected Sub lnkPrevious_Click(ByVal sender As Object, ByVal e As EventArgs) Handles lnkPrevious.Click
CurrentPage -= 1
LoadGrid()
End Sub
Protected Sub lnkNext_Click(ByVal sender As Object, ByVal e As EventArgs) Handles lnkNext.Click
CurrentPage += 1
LoadGrid()
End Sub
Protected Sub lnkbutGoToPage_Click(ByVal sender As Object, ByVal e As EventArgs) Handles lnkbutGoToPage.Click
CurrentPage = CInt(txtGoToPage.Text) - 1
LoadGrid()
End Sub
End Class
Como já vimos, a solução como um todo tem quatro projetos, expostos na Tabela 1. Os projetos têm poucas classes, expostas em um diagrama de classes na Figura 2. Nesta figura temos em vermelho as classes do projeto Web, em azul as classes de negócio, em verde as classes da camada de acesso a dados, em amarelo as classes da camada de dados e em roxo as classes da camada de utilidades. Utilizaremos LINQ to SQL para montar a camada de acesso a dados e a camada de entidades de negócio. Infelizmente a ferramenta impede a separação de projetos destas duas camadas, diferentemente do que é possível fazer hoje com os Datasets tipados no Visual Studio 2008, onde é possível colocar os datasets em um projeto diferente do projeto dos TableAdapters. O ideal seria termos estas camadas separadas também no LINQ to SQL, evitando assim uma referência direta entre o projeto de apresentação e de acesso a dados. Para melhor separá-los, separei os namespaces com a IDE do Visual Studio de LINQ to SQL, como veremos mais para frente.
A Figura 2 exibe também as dependências entre as camadas, ficando assim bem claro que uma classe depende explicitamente da outra para que possa funcionar, e que uma mudança na interface de uma classe pode afetar outra. O diagrama deixa claro que todas as classes dependem diretamente umas das outras. Dessa forma, qualquer mudança nas classes de DAL afeta toda a aplicação (isso quer dizer que ela precisaria ser inteiramente retestada em caso de uma mudança pontual). Estão ocultas as classes de Contact do namespace Dados (entidade de negócios, gerada pelo LINQ to SQL), e a classe Contact do namespace DAL (DAL ou Data Access Layer - camada de acesso a dados), para simplificar o diagrama. Sua responsabilidade é semelhante a da classe Order nos mesmos namespaces. A Tabela 2 anuncia a responsabilidade de cada classe.
Projeto |
Objetivo |
AppWeb |
Projeto do tipo Web application, responsável pela interface com o usuário. |
Business |
Projeto do tipo class library, responsável pela camada de negócio. |