Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

TDD per le viste

2,189 views

Published on

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

Published in: Technology
  • Be the first to comment

TDD per le viste

  1. 1. MatteoVaccari matteo.vaccari@xpeppers.com (cc) Alcuni diritti riservati TDD per le viste 1
  2. 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. 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. 4. It’s the design, baby! www.igiardinidiluca.eu 4
  5. 5. Model, view, controller Model View Controller 5
  6. 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. 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. 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. 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. 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. 11. Le GUI sono difficili? There is a lot of coding that goes into aVelocity template. But to useTDD for those templates would be absurd. ...Trying to do that fiddling withTDD 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. 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. 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. 14. La strategia usuale è di usare Selenium http://www.grahambrooks.com/ 14
  15. 15. Problemi con Selenium • Test lenti • Test fragili • Test che danno poco feedback sul design 15
  16. 16. Usa la forza degli oggetti, Luke! 16
  17. 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. 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. 19. How not to test • Fragile! @Test public void testParagraph() { Paragraph p = new Paragraph("ciao"); assertEquals("<p>ciao</p>", p.toHtml()); } 19
  20. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 36. Grazie dell’attenzione! Extreme Programming: sviluppo e mentoring 36

×