Testing GWT Applications

5,504 views
5,164 views

Published on

Many options are available for testing GWT applications, from pure Java tests to compiled GWTTestCases to full-scale functional tests. Choosing an appropriate testing method frequently requires a trade-off between speed and ease of creation on one hand, and coverage and realism on the other. This talk will discuss these techniques and tradeoffs and present some patterns and libraries to help you get the best of both worlds, as well as providing strategies for structuring GWT applications to maximize their testability.

Published in: Technology, News & Politics
0 Comments
3 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
5,504
On SlideShare
0
From Embeds
0
Number of Embeds
17
Actions
Shares
0
Downloads
95
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide

Testing GWT Applications

  1. 1. Testing GWT Applications Erik Kuefler, Google San Francisco, December 12-13th 2013 Google Confidential and Proprietary
  2. 2. Two Types of Tests ● Unit tests ● Test classes, methods and APIs ● Quick and focused ● Enable rapid iteration ● Functional tests ● Test applications and UIs ● Thorough and resilient ● Provide end-to-end confidence Google Confidential and Proprietary
  3. 3. Unit Tests Google Confidential and Proprietary
  4. 4. Unit Tests ● Test a single class or set of closely-related classes ● Mock out heavyweight dependencies ● Run very quickly Google Confidential and Proprietary
  5. 5. Should I Use GWTTestCase? ● Generally not as a first choice ● Compiling to javascript is sloooow ● Standard Java tools aren't available* ● Prefer to test code directly as Java when possible ● Still useful for testing JSNI and heavy DOM work *Check out EasyGwtMock though Google Confidential and Proprietary
  6. 6. class EmailField extends Composite { @UiField HasText textBox, message; private final EmailServiceAsync service = GWT.create(EmailService.class); @UiHandler("saveButton") void onSaveClicked(ClickEvent e) { if (RegExp.compile("[a-z]*@[a-z]*.com").test(textBox.getText())) { service.setEmail(textBox.getText(), new AsyncCallback<Void>() { @Override void onSuccess(Void result) { message.setText("Success!"); } @Override void onFailure(Throwable t) { message.setText("Error: " + t.getMessage()); } }); } // Else show an error... } } Google Confidential and Proprietary
  7. 7. class EmailField extends Composite { @UiField HasText textBox, message; How can I get instances of widgets? private final EmailServiceAsync service = GWT.create(EmailService.class); How can I emulate a button click? How can I create a fake service? @UiHandler("saveButton") void onSaveClicked(ClickEvent e) { How can if (RegExp.compile("[a-z]*@[a-z]*.com").test(textBox.getText())) { I verify service.setEmail(textBox.getText(), new AsyncCallback<Void>() { that a @Override void onSuccess(Void result) { service How can I set a value in a text box? was message.setText("Success!"); invoked How can I fake responses from the service? } (or not)? @Override void onFailure(Throwable t) { message.setText("Error: " + t.getMessage()); } }); How can I check changes to the DOM? } // Else show an error... } } Google Confidential and Proprietary
  8. 8. The Solution: (Gwt)Mockito ● We need a way to create a fake server and browser ● Mockito is a great library for generating mock objects ● GwtMockito automatically mocks GWT constructs ● No need to factor out an MVP-style view! Google Confidential and Proprietary
  9. 9. @RunWith(GwtMockitoTestRunner.class) public class EmailFieldTest { private final EmailField field = new EmailField(); @GwtMock private EmailServiceAsync service; @Test public void shouldSaveWellFormedAddresses() { when(field.textBox.getText()).thenReturn("me@foo.com"); doAnswer(asyncSuccess()).when(service).setEmail( anyString(), anyCallback()); field.onSaveClicked(new ClickEvent() {}); verify(field.message).setText("Success!"); } @Test public void shouldNotSaveMalformedAddresses() { when(field.textBox.getText()).thenReturn("bademail"); field.onSaveClicked(new ClickEvent() {}); verify(service, never()).setEmail(anyString(), anyCallback()); } } Google Confidential and Proprietary
  10. 10. @RunWith(GwtMockitoTestRunner.class) public class EmailFieldTest { Magical GWT-aware test runner private final EmailField field = new EmailField(); @GwtMock private EmailServiceAsync service; Reference the result of GWT.create @Test public void shouldSaveWellFormedAddresses() { when(field.textBox.getText()).thenReturn("me@foo.com"); doAnswer(asyncSuccess()).when(service).setEmail( anyString(), anyCallback());Mock service can be programmed to return field.onSaveClicked(new ClickEvent() {}); success or failure verify(field.message).setText("Success!"); } Package-private UiFields are filled with mocks @Test public void shouldNotSaveMalformedAddresses() { when(field.textBox.getText()).thenReturn("bademail"); UiHandlers can be called field.onSaveClicked(new ClickEvent() {}); directly (note the {}) verify(service, never()).setEmail(anyString(), anyCallback()); } } Google Confidential and Proprietary
  11. 11. Dealing With JSNI ● GwtMockito stubs native methods to be no-ops returning "harmless" values ● What to do when a no-op isn't enough? ● Best choice: dependency injection ● Last resort: overriding in tests private boolean calledDoJavascript = false; private MyWidget widget = new MyWidget() { @Override void doJavascript() { calledDoJavascript = true;} }; ● Fall back to GWTTestCase to test the actual JS Google Confidential and Proprietary
  12. 12. GwtMockito Summary ● Install via @RunWith(GwtMockitoTestRunner.class) ● Causes calls to GWT.create to return mocks or fakes ● Creates fake UiBinders that fill @UiFields with mocks ● Replaces JSNI methods with no-ops ● Removes final modifiers Google Confidential and Proprietary
  13. 13. Functional Tests Google Confidential and Proprietary
  14. 14. Functional tests ● Selenium/Webdriver tests that act like a user ● Provide implementation-independent tests ● Use either real or fake servers ● Appropriate for use-case-driven testing Google Confidential and Proprietary
  15. 15. Page Objects ● Provide a user-focused API for interacting with a widget ● Usually map 1:1 to GWT widgets ● Can contain other page objects ● All page object methods return one of: ● The page object itself (when there is no transition) ● Another page object (when there is a transition) ● A user-visible value from the UI (for assertions) Google Confidential and Proprietary
  16. 16. Google Confidential and Proprietary
  17. 17. Google Confidential and Proprietary
  18. 18. Google Confidential and Proprietary
  19. 19. public class AddCreditCardPage { private final String id; public AddCreditCardPage fillCreditCardNumber(String number) { wait().until(presenceOfElementLocated(By.id(id + Ids.CARD_NUMBER)) .sendKeys(number); return this; Wait for elements to be ready } public ReviewPage clickAddCreditCardButton() { wait().until(elementToBeClickable(By.id(id + Ids.ADD_CREDIT_CARD))) .click(); return new ReviewPage(Ids.REVIEW_PURCHASE); } Change pages by returning a new page object public String getErrorMessage() { return wait().until(presenceOfElementLocated(By.id(id + Ids.ERROR)) .getText(); } Always reference by ID } Google Confidential and Proprietary
  20. 20. Using Page Objects @Test public void shouldSelectCardsAfterAddingThem() { String selectedCard = new MerchantPage(webDriver) // Returns MerchantPage .clickBuy() // Returns ReviewPage .openCreditCardSelector() // Returns SelectorPage .selectAddCreditCardOption() // Returns AddCardPage .fillCreditCardNumber("4111111111110123") // Returns AddCardPage .fillCvc("456") // Returns AddCardPage .clickAddCreditCardButton() // Returns ReviewPage .openCreditCardSelector() .getSelectedItem(); // Returns SelectorPage // Returns String assertEquals("VISA 0123", selectedCard); } Note that the test never uses WebDriver/Selenium APIS! Google Confidential and Proprietary
  21. 21. Referring to Elements ● Page objects always reference elements by ID ● IDs are defined hierarchically: each level gives a new widget or page object ● Example ID: ".buyPage.creditCardForm.billingAddress.zip" ● Created via concatenation: find(By.id(myId + childId)); ● Page objects should never refer to grandchildren ● IDs set via ensureDebugId can be disabled in prod Google Confidential and Proprietary
  22. 22. Configuring IDs in GWT public class CreditCardFormWidget extends Composite { @Override protected void onEnsureDebugId(String baseId) { super.onEnsureDebugId(baseId); creditCardNumber.ensureDebugId(baseId + Ids.CARD_NUMBER); addressFormWidget.ensureDebugId(baseId + Ids.BILLING_ADDRESS); } } Shared between prod GWT code and test code public class Ids { public static final String CREDIT_CARD_FORM = ".ccForm"; public static final String CARD_NUMBER = ".ccNumber"; public static final String BILLING_ADDRESS = ".billingAddress"; } Google Confidential and Proprietary
  23. 23. Stubbing Servers public class RealServerConnection implements ServerConnection { @Override public void sendRequest( String url, String data, Callback callback) { RequestBuilder request = new RequestBuilder(RequestBuilder.POST, url); request.sendRequest(data, callback); } } public class StubServerConnection implements ServerConnection { @Override public native void sendRequest( String url, String data, Callback callback) /*-{ callback.Callback::onSuccess(Ljava/lang/String;)($wnd.stubData[url]); }-*/; } Read a canned response from a Javascript variable Google Confidential and Proprietary
  24. 24. Setting Up Deferred Binding <define-property name="serverType" values="real,stub"/> <set-property name="serverType" value="real"/> <replace-with class="my.package.RealServerConnection"> <when-type-is class="my.package.ServerConnection"/> <when-property-is name="serverType" value="real"/> </replace-with> <replace-with class="my.package.StubServerConnection"> <when-type-is class="my.package.ServerConnection"/> <when-property-is name="serverType" value="stub"/> </replace-with> Google Confidential and Proprietary
  25. 25. Setting Up Stubs In Tests @Test public void shouldShowContactsInRecipientAutocomplete() { new StubServer(webDriver).setContactData("John Doe", "Jane Doe", "Bob"); List<String> suggestions = new EmailPage(webDriver) .clickSendEmail() .setRecipients("Doe") .getAutocompleteSuggestions(); assertEquals(2, suggestions.size()); assertContains("John Doe", suggestions); assertContains("Jane Doe", suggestions); } public void setContactData(String... data) { ((JavascriptExecutor) webDriver).executeScript( "stubData['get_contact_data'] = arguments", data); } Google Confidential and Proprietary
  26. 26. Tips For Functional Tests ● Always use IDs, never xpath ● Wait, don't assert (or sleep) ● See org.openqa.selenium.support.ui.ExpectedConditions ● Never reference grandchildren in page objects ● Never use WebDriver APIs directly in tests ● Expose javascript APIs to functional tests judiciously Google Confidential and Proprietary
  27. 27. Please rate this presentation at gwtcreate.com/agenda! Q&A ekuefler@google.com https://github.com/ekuefler +Erik Kuefler Mockito: http://code.google.com/p/mockito/ GwtMockito: https://github.com/google/gwtmockito EasyGwtMock: https://code.google.com/p/easy-gwt-mock WebDriver: http://www.seleniumhq.org/projects/webdriver/ Google Confidential and Proprietary

×