Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TDD for web UIs

TDD for web UIs

How to use TDD and OOP to make "view" objects easier and cheaper to work with. In Italian. Presented at Italian Agile Day 2010 in Genova

Matteo Vaccari

November 19, 2010
Tweet

More Decks by Matteo Vaccari

Other Decks in Technology

Transcript

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

    viste 1
  2. 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
  3. 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
  4. It’s the design, baby! www.igiardinidiluca.eu 4

  5. Model, view, controller Model View Controller 5

  6. 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
  7. 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
  8. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/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_cr <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> E le viste?? 8
  9. E le viste?? <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> 9
  10. <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.i <%= render :partial => 'shared/discount', :locals => {:model => product_price, :form => pro 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"> E le viste?? 10
  11. 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-triage 11
  12. Le GUI sono una parte consistente delle app Righe di

    codice app/models app/controllers lib Totale non-gui app/views app/helpers Totale gui 2182 1604 2804 6590 6010 1085 7095 51,85% !!! 12
  13. 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
  14. La strategia usuale è di usare Selenium http://www.grahambrooks.com/ 14

  15. Problemi con Selenium • Test lenti • Test fragili •

    Test che danno poco feedback sul design 15
  16. Usa la forza degli oggetti, Luke! 16

  17. Trattiamo le viste come oggetti • Composizioni di oggetti che

    collaborano • Sono sviluppate in normalissimo Java (o Ruby o ...) • Testate unitariamente • Ben fattorizzate 17
  18. 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
  19. How not to test • Fragile! @Test public void testParagraph()

    { Paragraph p = new Paragraph("ciao"); assertEquals("<p>ciao</p>", p.toHtml()); } 19
  20. Testa xml, non stringhe @Test public void ignoresSmallDifferences() { assertDomEquals(

    "<div id='foo'></div>", "<div id=\"foo\" />" ); } // Depends on XMLUnit public 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
  21. 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
  22. Separa la creazione dall’uso @Override protected void service(HttpServletRequest request, HttpServletResponse

    response) thr 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
  23. 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 Servlet 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
  24. 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
  25. Isola il tuo codice da quello delle API esterne @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); SimpleRequest simpleRequest = new SimpleRequest(request); SimpleResponse simpleResponse = new SimpleResponse(response); application.process(simpleRequest, simpleResponse); } 25
  26. Così i test diventano facili FakeSimpleResponse 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. Test di “integrazione” senza Selenium @Test public 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
  33. Test di “integrazione” senza Selenium @Test public 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
  34. 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
  35. 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
  36. Grazie dell’attenzione! Extreme Programming: sviluppo e mentoring 36