testing gwt applications

27
Google Confidential and Proprietary Testing GWT Applications Erik Kuefler, Google San Francisco, December 12-13th 2013

Upload: erik-kuefler

Post on 10-May-2015

3.663 views

Category:

Technology


0 download

DESCRIPTION

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.

TRANSCRIPT

Page 1: Testing GWT Applications

Google Confidential and Proprietary

Testing GWT ApplicationsErik Kuefler, Google

San Francisco, December 12-13th 2013

Page 2: Testing GWT Applications

Google Confidential and Proprietary

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

Page 3: Testing GWT Applications

Google Confidential and Proprietary

Unit Tests

Page 4: Testing GWT Applications

Google Confidential and Proprietary

Unit Tests● Test a single class or set of closely-related classes

● Mock out heavyweight dependencies

● Run very quickly

Page 5: Testing GWT Applications

Google Confidential and Proprietary

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

Page 6: Testing GWT Applications

Google Confidential and Proprietary

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... }}

Page 7: Testing GWT Applications

Google Confidential and Proprietary

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... }}

How can I get instances of widgets?

How can I create a fake service? How can I emulate a button click?

How can I set a value in a text box?

How can I fake responses from the service?

How can I check changes to the DOM?

How can I verify that a service was invoked (or not)?

Page 8: Testing GWT Applications

Google Confidential and Proprietary

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!

Page 9: Testing GWT Applications

Google Confidential and Proprietary

@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("[email protected]"); 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()); }}

Page 10: Testing GWT Applications

Google Confidential and Proprietary

@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("[email protected]"); 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()); }}

Magical GWT-aware test runner

Reference the result of GWT.create

Package-private UiFields are filled with mocks

Mock service can be programmed to return success or failure

UiHandlers can be called directly (note the {})

Page 11: Testing GWT Applications

Google Confidential and Proprietary

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

● Fall back to GWTTestCase to test the actual JS

private boolean calledDoJavascript = false;private MyWidget widget = new MyWidget() { @Override void doJavascript() { calledDoJavascript = true;}};

Page 12: Testing GWT Applications

Google Confidential and Proprietary

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

Page 13: Testing GWT Applications

Google Confidential and Proprietary

Functional Tests

Page 14: Testing GWT Applications

Google Confidential and Proprietary

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

Page 15: Testing GWT Applications

Google Confidential and Proprietary

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)

Page 16: Testing GWT Applications

Google Confidential and Proprietary

Page 17: Testing GWT Applications

Google Confidential and Proprietary

Page 18: Testing GWT Applications

Google Confidential and Proprietary

Page 19: Testing GWT Applications

Google Confidential and Proprietary

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; } public ReviewPage clickAddCreditCardButton() { wait().until(elementToBeClickable(By.id(id + Ids.ADD_CREDIT_CARD))) .click(); return new ReviewPage(Ids.REVIEW_PURCHASE); } public String getErrorMessage() { return wait().until(presenceOfElementLocated(By.id(id + Ids.ERROR)) .getText(); }}

Always reference by ID

Wait for elements to be ready

Change pages by returning a new page object

Page 20: Testing GWT Applications

Google Confidential and Proprietary

@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() // Returns SelectorPage .getSelectedItem(); // Returns String

assertEquals("VISA 0123", selectedCard);}

Using Page Objects

Note that the test never uses WebDriver/Selenium APIS!

Page 21: Testing GWT Applications

Google Confidential and Proprietary

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

Page 22: Testing GWT Applications

Google Confidential and Proprietary

Configuring IDs in GWTpublic 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); }}

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";}

Shared between prod GWT code and test code

Page 23: Testing GWT Applications

Google Confidential and Proprietary

Stubbing Serverspublic 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

Page 24: Testing GWT Applications

Google Confidential and Proprietary

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>

Page 25: Testing GWT Applications

Google Confidential and Proprietary

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);}

Page 26: Testing GWT Applications

Google Confidential and Proprietary

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

Page 27: Testing GWT Applications

Google Confidential and Proprietary

Q & [email protected]://github.com/ekuefler+Erik Kuefler

Mockito: http://code.google.com/p/mockito/GwtMockito: https://github.com/google/gwtmockitoEasyGwtMock: https://code.google.com/p/easy-gwt-mockWebDriver: http://www.seleniumhq.org/projects/webdriver/

Please rate this presentation at gwtcreate.com/agenda!