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
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
It’s the design, baby!
www.igiardinidiluca.eu4
Model, view, controller
Model
View Controller
5
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
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
<!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
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
<% 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
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
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
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
La strategia usuale è di usare Selenium
http://www.grahambrooks.com/14
Problemi con Selenium
• Test lenti
• Test fragili
• Test che danno poco feedback sul design
15
Usa la forza degli oggetti, Luke!
16
Trattiamo le viste come oggetti
• Composizioni di oggetti che collaborano
• Sono sviluppate in normalissimo Java (o Ruby o ...)
• Testate unitariamente
• Ben fattorizzate
17
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
How not to test
• Fragile!
@Test public void testParagraph() { Paragraph p = new Paragraph("ciao"); assertEquals("<p>ciao</p>", p.toHtml());}
19
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
Grazie dell’attenzione!
Extreme Programming:sviluppo e mentoring
36
Top Related