Testing GWT Applications
-
Upload
erik-kuefler -
Category
Technology
-
view
3.663 -
download
0
description
Transcript of Testing GWT Applications
Google Confidential and Proprietary
Testing GWT ApplicationsErik Kuefler, Google
San Francisco, December 12-13th 2013
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
Google Confidential and Proprietary
Unit Tests
Google Confidential and Proprietary
Unit Tests● Test a single class or set of closely-related classes
● Mock out heavyweight dependencies
● Run very quickly
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
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... }}
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)?
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!
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()); }}
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 {})
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;}};
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
Google Confidential and Proprietary
Functional Tests
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
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)
Google Confidential and Proprietary
Google Confidential and Proprietary
Google Confidential and Proprietary
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
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!
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
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
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
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>
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);}
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
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!