36
Matteo Vaccari [email protected] (cc) Alcuni diritti riservati TDD per le viste 1

TDD per le viste

Embed Size (px)

DESCRIPTION

How to do test-driven development on the user interface of web applications

Citation preview

Page 1: TDD per le viste

Matteo [email protected]

(cc) Alcuni diritti riservati

TDD per le viste

1

Page 2: TDD per le viste

Chi son io?

• Ho sviluppato applicazioni web in PHP, Java, Ruby (on Rails)

• Lavoro in XPeppers come consulente e mentor

• Insegno Applicazioni Web I e II all’Insubria

2

Page 3: TDD per le viste

Qual’è l’obiettivo?

Rendere lo sviluppo sostenibile, nel senso che l’aggiunta o la modifica di feature

deve costare sempre di meno con il progredire del progetto

Bello! Come si fa?

3

Page 4: TDD per le viste

It’s the design, baby!

www.igiardinidiluca.eu4

Page 5: TDD per le viste

Model, view, controller

Model

View Controller

5

Page 6: TDD per le viste

Codice pulito nei controller

def list params[:page] ||= 1 orders = Order.find_all_by_id( params[:order_ids].split(",") ) @orders_count = orders.size @orders = orders.paginate(:page => params[:page], :per_page => 20) render :search end

def show @order = Order.find(params[:id]) @order_campaign = Campaign.find_by_name(@order.coupon_campaign_name) end

def by_number @order = Order.find_by_number(params[:number]) @order_campaign = Campaign.find_by_name(@order.coupon_campaign_name) @store = @order.store render :show end

6

Page 7: TDD per le viste

Codice pulito nei modelli

class Token < ActiveRecord::Base has_and_belongs_to_many :users, :uniq => true belongs_to :campaign validates_presence_of :code validate :code_is_unique validate :code_with_no_spaces

def Token.find_active(coupon_code) token = Token.find_by_code(coupon_code) end def blocking_requirements_given(user) if user unless can_use?(user) return [I18n.t(:'token.already_partecipated')] end end return [] end

7

Page 8: TDD per le viste

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="<%= I18n.locale.to_s %>"><%= render :partial => "shared/top", :locals => {:homepage => false} %><div id="category" class="grid_9"> <!-- site position --> <div id="position"> <%= render :partial => "shared/bread_crumb", :locals => { :category => @category, :bread_crumb => @bread_crumb } %> <div id="product-sorting"> <%= yield :product_sorting %> </div> </div> <!-- global elements for homepage --> <div id="global" class="grid_2 alpha"> <!-- shopping navigation --> <div id="shoppingnav"> <!-- basic navigation categories box --> <div id="deepening" <%= "class=\"hidden\"" if @category.second_level_with_no_children? %>> <div class="box"> <div class="container"> <div class="container"> <div class="container"> <h2 class="title"><%= t(:"into_category") %></h2> <div id="deepnav"> <ul class="nav"> <%- @categories.each do |category| -%> <li class="item"> <% open = category.id == @category.id ? "open" : ""%> <%= secure_link_to category.name, {:controller => "homepage", :action => "category", :id => category}, {:class => "link #{open}", :title => ""} %> </li> <%- end -%> </ul>

E le viste??

8

Page 9: TDD per le viste

E le viste??

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="<%= I18n.locale.to_s %>"><%= render :partial => "shared/top", :locals => {:homepage => false} %><div id="category" class="grid_9"> <!-- site position --> <div id="position"> <%= render :partial => "shared/bread_crumb", :locals => { :category => @category, :bread_crumb => @bread_crumb } %> <div id="product-sorting"> <%= yield :product_sorting %> </div> </div> <!-- global elements for homepage --> <div id="global" class="grid_2 alpha"> <!-- shopping navigation --> <div id="shoppingnav"> <!-- basic navigation categories box --> <div id="deepening" <%= "class=\"hidden\"" if @category.second_level_with_no_children? %>> <div class="box"> <div class="container"> <div class="container"> <div class="container"> <h2 class="title"><%= t(:"into_category") %></h2> <div id="deepnav"> <ul class="nav"> <%- @categories.each do |category| -%> <li class="item"> <% open = category.id == @category.id ? "open" : ""%> <%= secure_link_to category.name, {:controller => "homepage", :action => "category", :id => category}, {:class => "link #{open}", :title => ""} %> </li> <%- end -%> </ul> </div> </div> </div> </div> </div> </div> <!-- extra navigation categories box --> <%= yield :lower_sidebar_navigation_categories %> </div> </div> <!-- content --> <div id="content" class="grid_7 omega"> <div id="flash_messages"> <%= render :partial => 'shared/flash_messages', :locals => { :flash => flash } %> </div> <%= yield %> </div></div><div id="shop" class="grid_3"> <%= render :partial => 'shared/cart_preview' %></div><%= render :partial => "shared/bottom" %>

9

Page 10: TDD per le viste

<% content_for :head do %><%= javascript_include_tag 'tiny_mce/tiny_mce' %><%= javascript_include_tinymce %><% end %>

<h2>Editing product</h2>

<% form_for(@product) do |product_form| %>

<table id="product_details_edit"> <tr> <td colspan="2" style="text-align:center;"><%= product_form.error_messages %></td> </tr> <tr> <td id="main_image" width="20%"> <%= render :partial => 'product_images', :locals => { :product => @product } %> </td> <td width="80%" valign="top"> <h3><%= "#{@product.code} - #{@product.name_gestionale}" %></h3> <table width="100%"> <tr> <td id="price" class="product_edit"> <% product_price = @store.product_price_for(@product) %> <%= currency(product_price.price) %> </td> </tr> <tr> <td id="discount" class="product_edit boxed"> <div style="width: 45em;"> <div>Discount:</div> <% product_form.fields_for 'product_prices', product_price, :child_index => product_price.id do |product_price_form|%> <%= render :partial => 'shared/discount', :locals => {:model => product_price, :form => product_price_form, :show_discount_amount => true} %> <% end %> </div> </td> </tr> </table> <table width="100%" class="boxed"> <tr> <td class="product_edit" colspan="2"> <%= render :partial => 'product_variants_table', :locals => {:product => @product } %> </td> </tr> </table> </td> </tr> <tr> <td colspan="2"> <table id="details"> <tr> <th id="head_gestionale" class="product_edit"> Da Gestionale </th> <th id="head_actual" class="product_edit"> Visualizzato </th> </tr> <tr> <td id="name_gestionale" class="product_edit"> <%= @product.name_gestionale %> </td> <td id="name_actual" class="product_edit"> <%= product_form.text_field :name_actual, :size => 34, :disabled => false %> </td> </tr> <tr> <td id="description_gestionale" class="product_edit" valign="top" width="50%"> <%= @product.description_gestionale %> </td> <td id="description_actual" class="product_edit" width="50%"> <%= product_form.text_area :description_actual, :rows => 10, :cols => 30, :disabled => false %> </td> </tr> </table> </td> </tr> <tr> <td> </td> <td id="submit"> <%= product_form.submit 'Update' %> | <%= secure_link_to 'Reset data from gestionale', {:action => 'reset_data_from_gestionale', :id => @product}, :confirm => 'Really reset data from gestionale?' %> </td> </tr></table><% end %>

<%= secure_link_to 'Show', @product %> |<%= secure_link_to 'Admin Home', :controller => :products %>

E le viste??

10

Page 11: TDD per le viste

Le GUI sono difficili?

There is a lot of coding that goes into a Velocity template. But to use TDD for those templates would be absurd. ...Trying to do that fiddling with TDD is futile. Once I have the page the way I like it, then I’ll write some tests that make sure the templates

work as written.

-- Robert Martin

http://blog.objectmentor.com/articles/2009/10/08/tdd-triage11

Page 12: TDD per le viste

Le GUI sono una parte consistente delle app

Righe di codice

app/modelsapp/controllerslibTotale non-gui

app/viewsapp/helpers

Totale gui

2182160428046590

60101085

7095 51,85% !!!

12

Page 13: TDD per le viste

Rinunciare a fare TDD sulle viste conduce ad avere gran parte della

nostra applicazione che si oppone ai cambiamenti

Purtroppo è anche la parte che cambia più spesso

13

Page 14: TDD per le viste

La strategia usuale è di usare Selenium

http://www.grahambrooks.com/14

Page 15: TDD per le viste

Problemi con Selenium

• Test lenti

• Test fragili

• Test che danno poco feedback sul design

15

Page 16: TDD per le viste

Usa la forza degli oggetti, Luke!

16

Page 17: TDD per le viste

Trattiamo le viste come oggetti

• Composizioni di oggetti che collaborano

• Sono sviluppate in normalissimo Java (o Ruby o ...)

• Testate unitariamente

• Ben fattorizzate

17

Page 18: TDD per le viste

I template sono oggetti monchi

<td style="vertical-align:top;"> <h2>Products without images</h2> <table id="products_without_images" class ="index_table" cellpadding="0" cellspacing="0"> <tr> <% if @products_without_images.size > 0 %> <th class="narrow_column">Code</th > <th>Name</th > <% else %> <th>All products have images.</th> <% end %> </tr>

<% @products_without_images.each do |product| %> <tr class="<%= cycle("even", "odd") %>"> <td valign="top"><%= secure_link_to product.code, product, {:class => "product_link"} %> </td> <td valign="top"> <%=h product.name_actual %> </td> </tr> <% end %> </table></td>

• Hanno un solo “metodo”

• Difficile rimuovere le duplicazioni

• Difficile creare astrazioni

• Difficile testare la logica

18

Page 19: TDD per le viste

How not to test

• Fragile!

@Test public void testParagraph() { Paragraph p = new Paragraph("ciao"); assertEquals("<p>ciao</p>", p.toHtml());}

19

Page 20: TDD per le viste

Testa xml, non stringhe@Test public void ignoresSmallDifferences() { assertDomEquals( "<div id='foo'></div>", "<div id=\"foo\" />" );}

// Depends on XMLUnitpublic static void assertDomEquals(String expected, String actual) { try { XMLUnit.setIgnoreWhitespace(true); XMLAssert.assertXMLEqual(expected, actual); } catch (SAXException e) { fail(String.format("Malformed input: '%s'", actual)); }}

20

Page 21: TDD per le viste

Scomponi@Test public void textField() { TextField field = new TextField("A label", "a name", "a value") String expected = " <p>" + " <label for='a name'>A label:</label><br/>" + " <input type='text' name='a name' value='a value' />" + " </p>" + assertDomEquals(expected, field.toHtml());}

@Test public void formWithFields() { Form form = new Form("/an/action", "get"); TextField one = new TextField("Label", "name", "value"); TextField two = new TextField("Label", "name", "value"); form.addField(one); form.addField(two); String expected = "<form action='/an/action' method='get'>" + one.toHtml() + two.toHtml() + "</form>"; assertDomEquals(expected, form.toHtml());}

Questo test specifica come è fatto l’html di un campo di testo

Questo specifica lo html per una form

E non si rompe se cambia

l’html per il campo di testo

21

Page 22: TDD per le viste

Separa la creazione dall’uso

@Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { DataSource dataSource = new JndiDataSource("java:comp/env/jdbc/employees_db"); EmployeeRegistry registry = new JdbcEmployeeRegistry(dataSource); EmployeesApplication application = new EmployeesApplication(registry); application.process(request, response); }

22

Page 23: TDD per le viste

Isola il tuo codice da quello delle API esterne

public interface HttpServletRequest extends ServletRequest { public String getAuthType(); public Cookie[] getCookies(); public long getDateHeader(String name); public String getHeader(String name); public Enumeration getHeaders(String name); public Enumeration getHeaderNames();

// ... ~60 metodi

public interface HttpServletResponse extends ServletResponse { public void addCookie(Cookie cookie); public boolean containsHeader(String name); public String encodeURL(String url); public String encodeRedirectURL(String url); public String encodeUrl(String url); public String encodeRedirectUrl(String url); // .... ~50 metodi

23

Page 24: TDD per le viste

Isola il tuo codice da quello delle API esterne

public interface SimpleRequest { String getParameter(String name); String getSessionParameter(String name); String getRequestPath();}

public interface SimpleResponse { void redirectTo(String location); void render(HtmlComponent component);}

24

Page 25: TDD per le viste

Isola il tuo codice da quello delle API esterne

@Overrideprotected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { DataSource dataSource = new JndiDataSource("java:comp/env/jdbc/employees_db"); EmployeeRegistry registry = new JdbcEmployeeRegistry(dataSource); EmployeesApplication application = new EmployeesApplication(registry); SimpleRequest simpleRequest = new SimpleRequest(request); SimpleResponse simpleResponse = new SimpleResponse(response);

application.process(simpleRequest, simpleResponse);}

25

Page 26: TDD per le viste

Così i test diventano faciliFakeSimpleResponse response = new FakeSimpleResponse(); FakeEmployeeRegistry registry = new FakeEmployeeRegistry();FakeSimpleRequest request = new FakeSimpleRequest()EmployeeApplication app = new EmployeeApplication(registry);

@Test public void redirectsAfterInsert() { request.setParameter("name", "Un nome qualsiasi"); request.setParameter("salary", "3000"); request.setRequestPath("/employee/create"); app.process(request, response); assertEquals("/employees/list", resopnse.getRedirectLocation());}

26

Page 27: TDD per le viste

public class Display implements HtmlElement {

private String text;

public Display(String text) { this.text = text; }

public String toHtml() { return format("<p class='display'>%s</p>", text); }}

Sviluppa i tuoi componenti

27

Page 28: TDD per le viste

E poi specialìzzali

@Test public void displaysCurrentTime() throws Exception { Display display =

new TimeOfDayDisplay(new FakeClock(13, 45, TIME_ZONE_ROME)); assertEquals("It's 13:45 (Central European Time)", display.getText()); }

28

Page 29: TDD per le viste

Sviluppa i tuoi componenti

@Test public void returnsEmptyHtmlDocument() throws Exception { Page page = new Page(); String expected = Page.DOCTYPE + "<html>" + " <head>" + " <title></title>" + " </head>" + " <body>" + " </body>" + "</html>"; assertDomEquals(expected, page.toHtml()); }

29

Page 30: TDD per le viste

Sviluppa i tuoi componenti

@Test public void canHaveJavaScriptIncludes() throws Exception { Page page = new Page(); page.addJavaScriptInclude("one"); String expected = "<html>" + " <head>" + " <title></title>" + " <script type='text/javascript' src='/javascripts/one.js'></script>" + " </head>" + " <body>" + " </body>" + "</html>"; assertDomEquals(expected , page.toHtml()); }

30

Page 31: TDD per le viste

Test “a specchio”

@Test public void canHaveExternalStylesheets() throws Exception { Page page = new Page();

Display display = new Display(); page.addComponent(display); String expected = "<html>" + " <head>" + " <title></title>" + " </head>" + " <body>" + display().toHtml(); " </body>" + "</html>"; assertDomEquals(expected , page.toHtml()); }

31

Page 32: TDD per le viste

Test di “integrazione” senza Selenium

@Testpublic void enteringNewEmployee() throws Exception { List<Employee> employees = new ArrayList<Employee>(); EmployeesApplication application = new EmployeesApplication(employees); User user = new User();

user.visit(application, "/employees"); user.enter("name", "Mario Rossi"); user.enter("salary", "1234"); user.click("OK");

assertThat(employees.size(), is(1)); assertThat(employees.get(0), is(new Employee("Mario Rossi", new Money(123400))));}

32

Page 33: TDD per le viste

Test di “integrazione” senza Selenium

@Testpublic void enteringNewEmployee() throws Exception { List<Employee> employees = new ArrayList<Employee>(); EmployeesApplication application = new EmployeesApplication(employees); User user = new User();

user.visit(application, "/employees"); user.enter("name", "Mario Rossi"); user.enter("salary", "1234"); user.click("OK");

assertThat(employees.size(), is(1)); assertThat(employees.get(0), is(new Employee("Mario Rossi", new Money(123400))));}

Verifica che la form contenga

effettivamente questi due campi

Simula un click sull'applicazione

Simula una richiesta

33

Page 34: TDD per le viste

Tutto in 40 righe di codice

public void click(String buttonName) { XmlDocument formNode = document.getNode("//form"); document.getNode("//form//input[@type='submit'][@value='%s']", buttonName); String action = formNode.getAttribute("action"); String method = formNode.getAttribute("method"); application.service(new SimpleRequest(method, params, action)); }

public void enter(String name, String value) { try { document.getNode("//form//input[@name='%s']", name); } catch (ElementNotFoundException e) { throw new ElementNotFoundException("No field with name '" + name + "'", e); } this.params.add(name, value); }

34

Page 35: TDD per le viste

In conclusione?

• TDD per le viste: si... può.... fare!!!

• Usa la forza degli oggetti

• Si può ottenere 90% del valore di Selenium con test puramente unitari

• Templates considered harmful.

35

Page 36: TDD per le viste

Grazie dell’attenzione!

Extreme Programming:sviluppo e mentoring

36