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!


Top Related