TDD per le viste

2,050 views

Published on

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

Published in: Technology
0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
2,050
On SlideShare
0
From Embeds
0
Number of Embeds
227
Actions
Shares
0
Downloads
21
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

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

×