谷歌 scott-lessons learned in testability

54
Lessons Learned in Testability Scott McMaster Google Kirkland, Washington USA

Upload: drewz-lin

Post on 19-May-2015

207 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: 谷歌 Scott-lessons learned in testability

Lessons Learned in Testability

Scott McMaster

Google

Kirkland, Washington USA

Page 2: 谷歌 Scott-lessons learned in testability

About Me

• Software Design Engineer @ Google.

– Building high-traffic web frontends and services in Java.

– AdWords, Google Code

• Ph.D. in Computer Science, U. of Maryland.

• Formerly of Amazon.com (2 years), Lockheed Martin (2 years), Microsoft

(7 years), and some small startups.

• Frequent adjunct professor @ Seattle University, teaching software

design, architecture, and OO programming.

• Author of technical blog at http://www.scottmcmaster365.com.

Page 3: 谷歌 Scott-lessons learned in testability

Testing and Me

• Doing automated testing since 1995.

• Ph.D. work in test coverage and test suite maintenance.

• Champion of numerous unit, system, and performance

testing tools and techniques.

• Co-founder of WebTestingExplorer open-source automated

web testing framework (www.webtestingexplorer.org).

Page 4: 谷歌 Scott-lessons learned in testability

Agenda

• What is Testability?

• Testability Sins

– Statics and singletons

– Mixing Business and Presentation Logic

– Breaking the Law of Demeter

• Testability Solutions

– Removing singletons.

– Asking for Dependencies

– Dependency Injection

– Mocks and Fakes

– Refactoring to UI Patterns

Page 5: 谷歌 Scott-lessons learned in testability

Testability: Formal Definition

• Wikipedia: “the degree to which a software

artifact (i.e. a software system, software

module, requirements- or design document)

supports testing in a given test context.”

http://en.wikipedia.org/wiki/Software_testability

Page 6: 谷歌 Scott-lessons learned in testability

Some Aspects of Testability

• Controllable: We can put the software in a state to begin

testing.

• Observable: We can see things going right (or wrong).

• Isolatable: We can test classes/modules/systems apart from

others.

• Automatable: We can write or generate automated tests.

– Requires each of the previous three to some degree.

Page 7: 谷歌 Scott-lessons learned in testability

Testability: More Practical Definition

• Testability is a function of your testing goals.

• Our primary goal is to write or generate automated tests.

• Therefore, testability is the ease with which we can write:

– Unit tests

– System tests

– End-to-end tests

Page 8: 谷歌 Scott-lessons learned in testability

Testers and Testability• At Google, test engineers:

– Help ensure that developers build testable

software.

– Provide guidance to developers on best practices

for unit and end-to-end testing.

– May participate in refactoring production code for

testability.

Page 9: 谷歌 Scott-lessons learned in testability

Example: Weather App

Page 10: 谷歌 Scott-lessons learned in testability

Weather App Architecture

Rich Browser UI (GWT)

Frontend Server (GWT RPC servlet)

Remote Web Service (XML-over-

HTTP)User Database

Page 11: 谷歌 Scott-lessons learned in testability

Original Weather App Design

Page 12: 谷歌 Scott-lessons learned in testability

Testability Problems?

• Can’t test without calling the cloud service.

– Slow to run, unstable.

• Can’t test any client-side components without

loading a browser or browser emulator.

– Slow to develop, slow to run, perhaps unstable.

Page 13: 谷歌 Scott-lessons learned in testability

Mission #1: Unit Tests for WeatherServiceImpl• Problem: Uses static singleton reference to

GlobalWeatherService, can’t be tested in isolation.

• Solution:

– Eliminate the static singleton.

– Pass a mock or stub to the WeatherServiceImpl

constructor at test-time.

Page 14: 谷歌 Scott-lessons learned in testability

WeatherServiceImpl: Beforeprivate private private private static static static static GlobalWeatherServiceGlobalWeatherServiceGlobalWeatherServiceGlobalWeatherService service = new service = new service = new service = new GlobalWeatherServiceGlobalWeatherServiceGlobalWeatherServiceGlobalWeatherService();();();();

public public public public List<String> List<String> List<String> List<String> getCitiesForCountrygetCitiesForCountrygetCitiesForCountrygetCitiesForCountry(String (String (String (String countryNamecountryNamecountryNamecountryName) {) {) {) { try try try try {{{{ if if if if ((((countryNamecountryNamecountryNamecountryName == null || == null || == null || == null || countryName.isEmptycountryName.isEmptycountryName.isEmptycountryName.isEmpty()) {()) {()) {()) { return return return return new new new new ArrayListArrayListArrayListArrayList<String>();<String>();<String>();<String>(); } return return return return service.getCitiesForCountryservice.getCitiesForCountryservice.getCitiesForCountryservice.getCitiesForCountry((((countryNamecountryNamecountryNamecountryName))));;;; } catch (Exception e) {catch (Exception e) {catch (Exception e) {catch (Exception e) { throw throw throw throw new new new new RuntimeExceptionRuntimeExceptionRuntimeExceptionRuntimeException(e);(e);(e);(e); }}

What if we try to test this in its current form?1. GlobalWeatherService gets loaded at classload-time.

1. This itself could be slow or unstable depending on the implementation.2. When we call getCititesForCountry(“China”), a remote web service call gets made.3. This remote web service call may:

1. Fail.2. Be really slow.3. Not return predictable results.� Any of these things can make our test “flaky”.

Page 15: 谷歌 Scott-lessons learned in testability

Proposed Solution

• First we need to get rid of the static singleton.

• Then we need something that:

– Behaves like GlobalWebService.

– Is fast and predictable.

– Can be inserted into WeatherServiceImpl at test-

time.

Page 16: 谷歌 Scott-lessons learned in testability

A Word About Static Methods and Singletons• Never use them!

• They are basically global variables (and we’ve all

been taught to avoid those).

• They are hard to replace with alternative

implementations, mocks, and stubs/fakes.

– They make automated unit testing extremely difficult.

Page 17: 谷歌 Scott-lessons learned in testability

Scott’s Rules About Static Methods and Singletons

1. Avoid static methods.

2. For classes that are logically “singleton”, make

them non-singleton instances and manage

them in a dependency injection container (more

on this shortly).

Page 18: 谷歌 Scott-lessons learned in testability

Singleton Removal

public class public class public class public class WeatherServiceImplWeatherServiceImplWeatherServiceImplWeatherServiceImpl extends extends extends extends RemoteServiceServletRemoteServiceServletRemoteServiceServletRemoteServiceServlet implements implements implements implements WeatherService {

private private private private final final final final GlobalWeatherServiceGlobalWeatherServiceGlobalWeatherServiceGlobalWeatherService service; service; service; service;

public public public public WeatherServiceImplWeatherServiceImplWeatherServiceImplWeatherServiceImpl((((GlobalWeatherServiceGlobalWeatherServiceGlobalWeatherServiceGlobalWeatherService service) { service) { service) { service) { this.servicethis.servicethis.servicethis.service = service;= service;= service;= service; } ...

• Also, make GlobalWeatherService into an interface.• Now we can pass in a special implementation for unit

testing.• But we have a big problem…

Page 19: 谷歌 Scott-lessons learned in testability

We’ve Broken Our Service!

• The servlet container does not understand

how to create WeatherServiceImpl anymore.

– Its constructor takes a funny parameter.

• The solution?

Page 20: 谷歌 Scott-lessons learned in testability

Dependency Injection• Can be a little complicated, but here is what you

need to know here:

• Accept your dependencies, don’t ask for them.

– Then your dependencies can be replaced (generally, with

simpler implementations) at test time.

– In production, your dependencies get inserted by a

dependency injection container.

• In Java, this is usually Spring or Google Guice.

Page 21: 谷歌 Scott-lessons learned in testability

Dependency Injection with Google Guice

• Google Guice: A Dependency Injection framework.

• When properly set up, it will create your objects and pass

them to the appropriate constructors at runtime, freeing you

up to do other things with the constructors at test-time.

• Setting up Guice is outside the scope of this talk.

– This will get you started: http://code.google.com/p/google-

guice/wiki/Servlets

Page 22: 谷歌 Scott-lessons learned in testability

Fixing WeatherServiceImpl (1)

•Configure our servlet to use Guice and tell it about our objects:

•When someone asks for a “GlobalWeatherService”, Guice will

give it an instance of GlobalWeatherServiceImpl.

public class public class public class public class WeatherAppModuleWeatherAppModuleWeatherAppModuleWeatherAppModule extends extends extends extends AbstractModuleAbstractModuleAbstractModuleAbstractModule { { { { @Override protected protected protected protected void configure() {void configure() {void configure() {void configure() { bind(WeatherServiceImpl.classclassclassclass);););); bind(GlobalWeatherService.classclassclassclass).to().to().to().to(GlobalWeatherServiceImpl.classGlobalWeatherServiceImpl.classGlobalWeatherServiceImpl.classGlobalWeatherServiceImpl.class);););); }}

Page 23: 谷歌 Scott-lessons learned in testability

Fixing WeatherServiceImpl (2)

• At runtime, Guice will create our servlet and the object(s) it

needs:

@Singletonpublic class public class public class public class WeatherServiceImplWeatherServiceImplWeatherServiceImplWeatherServiceImpl extends extends extends extends RemoteServiceServletRemoteServiceServletRemoteServiceServletRemoteServiceServlet implements implements implements implements WeatherService {

private private private private final final final final GlobalWeatherServiceGlobalWeatherServiceGlobalWeatherServiceGlobalWeatherService service; service; service; service;

@Inject public public public public WeatherServiceImplWeatherServiceImplWeatherServiceImplWeatherServiceImpl((((GlobalWeatherServiceGlobalWeatherServiceGlobalWeatherServiceGlobalWeatherService service) { service) { service) { service) { this.servicethis.servicethis.servicethis.service = service;= service;= service;= service; } ...

The “@Inject” constructor parameters is how we ask Guice for instances.

Page 24: 谷歌 Scott-lessons learned in testability

After Testability Refactoring #1

Page 25: 谷歌 Scott-lessons learned in testability

Finally! We Can Test!• But how?

• We want to test WeatherServiceImpl in

isolation.

⇒For GlobalWeatherService, we only care about

how it interacts with WeatherServiceImpl.

⇒To create the proper interactions, a mock object mock object mock object mock object

is ideal

Page 26: 谷歌 Scott-lessons learned in testability

Mock Object Testing

• Mock objects simulate real objects in ways specified by the tester.

• The mock object framework verifies these interactions occur as expected.

– A useful consequence of this: If appropriate, you can verify that an application

is not making more remote calls than expected.

– Another useful consequence: Mocks make it easy to test exception handling.

• Common mocking frameworks (for Java):

– Mockito

– EasyMock

• I will use this.

Page 27: 谷歌 Scott-lessons learned in testability

Using Mock Objects1. Create a mock object.

2. Set up expectations:

1. How we expect the class-under-test to call it.

2. What we want it to return.

3. “Replay” the mock.

4. Invoke the class-under-test.

5. “Verify” the mock interactions were as-expected.

Page 28: 谷歌 Scott-lessons learned in testability

Testing with a Mock Objectprivate private private private GlobalWeatherServiceGlobalWeatherServiceGlobalWeatherServiceGlobalWeatherService globalWeatherServiceglobalWeatherServiceglobalWeatherServiceglobalWeatherService;;;;private private private private WeatherServiceImplWeatherServiceImplWeatherServiceImplWeatherServiceImpl weatherServiceweatherServiceweatherServiceweatherService;;;;

@@@@BeforeBeforeBeforeBeforepublic public public public void void void void setUpsetUpsetUpsetUp() {() {() {() { globalWeatherServiceglobalWeatherServiceglobalWeatherServiceglobalWeatherService = = = = EasyMock.createMockEasyMock.createMockEasyMock.createMockEasyMock.createMock((((GlobalWeatherService.classGlobalWeatherService.classGlobalWeatherService.classGlobalWeatherService.class);););); weatherServiceweatherServiceweatherServiceweatherService = new = new = new = new WeatherServiceImplWeatherServiceImplWeatherServiceImplWeatherServiceImpl((((globalWeatherServiceglobalWeatherServiceglobalWeatherServiceglobalWeatherService););););}}}}

@@@@TestTestTestTestpublic public public public void void void void testGetCitiesForCountry_nonEmptytestGetCitiesForCountry_nonEmptytestGetCitiesForCountry_nonEmptytestGetCitiesForCountry_nonEmpty() throws Exception {() throws Exception {() throws Exception {() throws Exception { EasyMock.expectEasyMock.expectEasyMock.expectEasyMock.expect((((globalWeatherService.getCitiesForCountryglobalWeatherService.getCitiesForCountryglobalWeatherService.getCitiesForCountryglobalWeatherService.getCitiesForCountry("china"))("china"))("china"))("china")) . . . .andReturnandReturnandReturnandReturn((((ImmutableList.ofImmutableList.ofImmutableList.ofImmutableList.of("("("("beijingbeijingbeijingbeijing", "shanghai"));", "shanghai"));", "shanghai"));", "shanghai")); EasyMock.replayEasyMock.replayEasyMock.replayEasyMock.replay((((globalWeatherServiceglobalWeatherServiceglobalWeatherServiceglobalWeatherService);););); List List List List<String> cities = <String> cities = <String> cities = <String> cities = weatherService.getCitiesForCountryweatherService.getCitiesForCountryweatherService.getCitiesForCountryweatherService.getCitiesForCountry("china");("china");("china");("china"); assertEqualsassertEqualsassertEqualsassertEquals(2, (2, (2, (2, cities.sizecities.sizecities.sizecities.size());());());()); assertTrueassertTrueassertTrueassertTrue((((cities.containscities.containscities.containscities.contains("("("("beijingbeijingbeijingbeijing"));"));"));")); assertTrueassertTrueassertTrueassertTrue((((cities.containscities.containscities.containscities.contains("shanghai"));("shanghai"));("shanghai"));("shanghai")); EasyMock.verifyEasyMock.verifyEasyMock.verifyEasyMock.verify((((globalWeatherServiceglobalWeatherServiceglobalWeatherServiceglobalWeatherService););););}}}}

Observe:• How we take advantage of the new WeatherServiceImpl constructor.• How we use the mock GlobalWeatherService.

Page 29: 谷歌 Scott-lessons learned in testability

Mission #2: Unit Tests for GlobalWeatherService

• Problem: Talks to external web service, does non-trivial XML

processing that we want to test.

• Solution:

– Split the remote call from the XML processing.

– Wrap external web service in an object with a known interface.

– Pass an instance to the GlobalWeatherServiceImpl constructor.

– Use dependency injection to create the real object at runtime, use a

fake at test-time.

Page 30: 谷歌 Scott-lessons learned in testability

After Testability Refactoring #2

Page 31: 谷歌 Scott-lessons learned in testability

Fakes vs. Mocks

• Mocks

– Verifies behavior (expected calls).

– Implementation usually generated by a mock object framework.

– Often only usable in a single test case.

– Often fragile as the implementation changes.

• Fakes

– Contains a simplified implementation of the real thing (perhaps using static data, an in-memory

database, etc.).

– Implementation usually generated by hand.

– Often reusable across test cases and test suites if carefully designed.

• Mocks and Fakes

– Either can often be used in a given situation.

– But some situations lend themselves to one more than the other.

Page 32: 谷歌 Scott-lessons learned in testability

Use a Fake, or Use a Mock?• Problem: Setting up a mock object for

GlobalWeatherDataAccess that returns static XML is

possible, but ugly (and perhaps not very reusable).

• Idea: Create a fakefakefakefake implementation of

GlobalWeatherDataAccess.

– We can give the fake object the capability to return

different XML in different test circumstances.

Page 33: 谷歌 Scott-lessons learned in testability

Implementing the Fake Objectpublic class public class public class public class FakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccess implements implements implements implements GlobalWeatherDataAccessGlobalWeatherDataAccessGlobalWeatherDataAccessGlobalWeatherDataAccess { { { {

/ / / // Try http:/// Try http:/// Try http:/// Try http://www.htmlescape.netwww.htmlescape.netwww.htmlescape.netwww.htmlescape.net////javaescape_tool.htmljavaescape_tool.htmljavaescape_tool.htmljavaescape_tool.html to generate these. to generate these. to generate these. to generate these. private private private private static final String CHINA_CITIES = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String CHINA_CITIES = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String CHINA_CITIES = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String CHINA_CITIES = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string xmlnsxmlnsxmlnsxmlns=\"http://=\"http://=\"http://=\"http://www.webserviceX.NETwww.webserviceX.NETwww.webserviceX.NETwww.webserviceX.NET\"><\"><\"><\"><NewDataSetNewDataSetNewDataSetNewDataSet>\n <Table>\n <Country>China</Country>\n <City>Beijing</City>\n </Table>\n <Table>\n <Country>China</Country>\n <City>Shanghai</City>\n </Table>\n</>\n <Table>\n <Country>China</Country>\n <City>Beijing</City>\n </Table>\n <Table>\n <Country>China</Country>\n <City>Shanghai</City>\n </Table>\n</>\n <Table>\n <Country>China</Country>\n <City>Beijing</City>\n </Table>\n <Table>\n <Country>China</Country>\n <City>Shanghai</City>\n </Table>\n</>\n <Table>\n <Country>China</Country>\n <City>Beijing</City>\n </Table>\n <Table>\n <Country>China</Country>\n <City>Shanghai</City>\n </Table>\n</NewDataSetNewDataSetNewDataSetNewDataSet></string>";></string>";></string>";></string>"; private private private private static final String BEIJING_WEATHER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String BEIJING_WEATHER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String BEIJING_WEATHER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String BEIJING_WEATHER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string xmlnsxmlnsxmlnsxmlns=\"http://=\"http://=\"http://=\"http://www.webserviceX.NETwww.webserviceX.NETwww.webserviceX.NETwww.webserviceX.NET\">&\">&\">&\">&ltltltlt;?xml version=\"1.0\" encoding=\"utf-16\"?&;?xml version=\"1.0\" encoding=\"utf-16\"?&;?xml version=\"1.0\" encoding=\"utf-16\"?&;?xml version=\"1.0\" encoding=\"utf-16\"?&gtgtgtgt;\;\;\;\n&lt;CurrentWeather&gtn&lt;CurrentWeather&gtn&lt;CurrentWeather&gtn&lt;CurrentWeather&gt;\n &;\n &;\n &;\n &lt;Location&gt;Beijinglt;Location&gt;Beijinglt;Location&gt;Beijinglt;Location&gt;Beijing, China (ZBAA) 39-56N 116-17E 55M&lt;/, China (ZBAA) 39-56N 116-17E 55M&lt;/, China (ZBAA) 39-56N 116-17E 55M&lt;/, China (ZBAA) 39-56N 116-17E 55M&lt;/Location&gtLocation&gtLocation&gtLocation&gt;\n &;\n &;\n &;\n &lt;Time&gt;Octlt;Time&gt;Octlt;Time&gt;Octlt;Time&gt;Oct 27, 2012 - 04:00 PM EDT / 2012.10.27 2000 27, 2012 - 04:00 PM EDT / 2012.10.27 2000 27, 2012 - 04:00 PM EDT / 2012.10.27 2000 27, 2012 - 04:00 PM EDT / 2012.10.27 2000 UTC&ltUTC&ltUTC&ltUTC&lt;/;/;/;/Time&gtTime&gtTime&gtTime&gt;\n &;\n &;\n &;\n &lt;Wind&gtlt;Wind&gtlt;Wind&gtlt;Wind&gt; from the N (010 degrees) at 9 MPH (8 KT):0&lt;/; from the N (010 degrees) at 9 MPH (8 KT):0&lt;/; from the N (010 degrees) at 9 MPH (8 KT):0&lt;/; from the N (010 degrees) at 9 MPH (8 KT):0&lt;/Wind&gtWind&gtWind&gtWind&gt;\n ;\n ;\n ;\n &&&&lt;Visibility&gtlt;Visibility&gtlt;Visibility&gtlt;Visibility&gt; greater than 7 mile(s):0&lt;/; greater than 7 mile(s):0&lt;/; greater than 7 mile(s):0&lt;/; greater than 7 mile(s):0&lt;/Visibility&gtVisibility&gtVisibility&gtVisibility&gt;\n &;\n &;\n &;\n &lt;Temperature&gtlt;Temperature&gtlt;Temperature&gtlt;Temperature&gt; 39 F (4 C)&; 39 F (4 C)&; 39 F (4 C)&; 39 F (4 C)&ltltltlt;/;/;/;/Temperature&gtTemperature&gtTemperature&gtTemperature&gt;\n &;\n &;\n &;\n &lt;DewPoint&gtlt;DewPoint&gtlt;DewPoint&gtlt;DewPoint&gt; 28 F (-2 C)&; 28 F (-2 C)&; 28 F (-2 C)&; 28 F (-2 C)&ltltltlt;/;/;/;/DewPoint&gtDewPoint&gtDewPoint&gtDewPoint&gt;\n &;\n &;\n &;\n &lt;RelativeHumidity&gtlt;RelativeHumidity&gtlt;RelativeHumidity&gtlt;RelativeHumidity&gt; 64%&; 64%&; 64%&; 64%&ltltltlt;/;/;/;/RelativeHumidity&gtRelativeHumidity&gtRelativeHumidity&gtRelativeHumidity&gt;\n &;\n &;\n &;\n &lt;Pressure&gtlt;Pressure&gtlt;Pressure&gtlt;Pressure&gt; 30.30 in. Hg (1026 ; 30.30 in. Hg (1026 ; 30.30 in. Hg (1026 ; 30.30 in. Hg (1026 hPahPahPahPa)&)&)&)&ltltltlt;/;/;/;/Pressure&gtPressure&gtPressure&gtPressure&gt;\n &;\n &;\n &;\n &lt;Status&gt;Success&ltlt;Status&gt;Success&ltlt;Status&gt;Success&ltlt;Status&gt;Success&lt;/;/;/;/Status&gtStatus&gtStatus&gtStatus&gt;\;\;\;\n&ltn&ltn&ltn&lt;/;/;/;/CurrentWeather&gtCurrentWeather&gtCurrentWeather&gtCurrentWeather&gt;</string>";;</string>";;</string>";;</string>"; private private private private static final String NO_CITIES = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String NO_CITIES = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String NO_CITIES = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string static final String NO_CITIES = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string xmlnsxmlnsxmlnsxmlns=\"http://=\"http://=\"http://=\"http://www.webserviceX.NETwww.webserviceX.NETwww.webserviceX.NETwww.webserviceX.NET\"><\"><\"><\"><NewDataSetNewDataSetNewDataSetNewDataSet /></string>"; /></string>"; /></string>"; /></string>";

@ @ @ @OverrideOverrideOverrideOverride public public public public String String String String getCitiesForCountryXmlgetCitiesForCountryXmlgetCitiesForCountryXmlgetCitiesForCountryXml(String (String (String (String countryNamecountryNamecountryNamecountryName) throws Exception {) throws Exception {) throws Exception {) throws Exception { if if if if ("("("("china".equalschina".equalschina".equalschina".equals((((countryName.toLowerCasecountryName.toLowerCasecountryName.toLowerCasecountryName.toLowerCase())) {())) {())) {())) { return return return return CHINA_CITIES;CHINA_CITIES;CHINA_CITIES;CHINA_CITIES; } } } } return return return return NO_CITIES;NO_CITIES;NO_CITIES;NO_CITIES; } } } }

@ @ @ @OverrideOverrideOverrideOverride public public public public String String String String getWeatherForCityXmlgetWeatherForCityXmlgetWeatherForCityXmlgetWeatherForCityXml(String (String (String (String countryNamecountryNamecountryNamecountryName, String , String , String , String cityNamecityNamecityNamecityName)))) throws throws throws throws Exception {Exception {Exception {Exception { return return return return BEIJING_WEATHER;BEIJING_WEATHER;BEIJING_WEATHER;BEIJING_WEATHER; } } } }}}}}

Page 34: 谷歌 Scott-lessons learned in testability

Testing with a Fake Objectprivate private private private GlobalWeatherServiceImplGlobalWeatherServiceImplGlobalWeatherServiceImplGlobalWeatherServiceImpl globalWeatherServiceglobalWeatherServiceglobalWeatherServiceglobalWeatherService;;;;private private private private FakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccess dataAccessdataAccessdataAccessdataAccess;;;;

@Beforepublic public public public void void void void setUpsetUpsetUpsetUp() {() {() {() { dataAccess = new new new new FakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccessFakeGlobalWeatherDataAccess();();();(); globalWeatherService = new new new new GlobalWeatherServiceImplGlobalWeatherServiceImplGlobalWeatherServiceImplGlobalWeatherServiceImpl((((dataAccessdataAccessdataAccessdataAccess););););}

@Testpublic public public public void void void void testGetCitiesForCountry_nonEmptytestGetCitiesForCountry_nonEmptytestGetCitiesForCountry_nonEmptytestGetCitiesForCountry_nonEmpty() throws Exception {() throws Exception {() throws Exception {() throws Exception { List<String> cities = globalWeatherService.getCitiesForCountry("china"); assertEquals(2, cities.size()); assertTrue(cities.contains("beijing")); assertTrue(cities.contains("shanghai"));}

@Testpublic public public public void void void void testGetCitiesForCountry_emptytestGetCitiesForCountry_emptytestGetCitiesForCountry_emptytestGetCitiesForCountry_empty() throws Exception {() throws Exception {() throws Exception {() throws Exception { List<String> cities = globalWeatherService.getCitiesForCountry("nowhere"); assertTrue(cities.isEmpty());}

The fake keeps the tests short, simple, and to-the-point!

Page 35: 谷歌 Scott-lessons learned in testability

Mission #3: Unit Tests for WeatherHome

• Problem: UI and business logic / service calls all mixed

together.

– The view layer is difficult and slow to instantiate at unit test-time.

– But we need to unit test the business logic.

• Solution:

– Refactor to patterns -- Model-View-Presenter (MVP).

– Write unit tests for the Presenter using a mock or stub View.

Page 36: 谷歌 Scott-lessons learned in testability

Mixing Business and Presentation@UiHandler("login")void void void void onLoginonLoginonLoginonLogin((((ClickEventClickEventClickEventClickEvent e) { e) { e) { e) { weatherService.getWeatherForUser(userName.getText(), new new new new AsyncCallbackAsyncCallbackAsyncCallbackAsyncCallback<Weather>() {<Weather>() {<Weather>() {<Weather>() {

@Override public public public public void void void void onFailureonFailureonFailureonFailure((((ThrowableThrowableThrowableThrowable caught) caught) caught) caught) {{{{ Window.alert("oops");

}

@Override public public public public voidvoidvoidvoid onSuccessonSuccessonSuccessonSuccess((((WeatherWeatherWeatherWeather weatherweatherweatherweather) {) {) {) { if if if if (weather != null) {(weather != null) {(weather != null) {(weather != null) { fillWeather(weather); unknownUser.setVisible(false);false);false);false); } elseelseelseelse { { { { unknownUser.setVisible(true);true);true);true); } } });}

How NOT to write a UI event handler for maximum testability:• Have tight coupling between the UI event, processing a remote service call, and

updating the UI.

Page 37: 谷歌 Scott-lessons learned in testability

Model-View-Presenter (MVP)

• UI pattern that separates business and

presentation logic.

• Makes the View easier to modify.

• Makes the business logic easier to test by

isolating it in the Presenter.

Page 38: 谷歌 Scott-lessons learned in testability

Model-View-Presenter Overview

Page 39: 谷歌 Scott-lessons learned in testability

Model-View-Presenter Responsibilities

• Presenter uses the View interface to

manipulate the UI.

• View delegates UI event handling back to the

Presenter via an event bus or an interface.

• Presenter handles all service calls and

reading/updating of the Model.

Page 40: 谷歌 Scott-lessons learned in testability

Passive View MVP• A particular style of MVP where the View is

completely passive, only defining and layout and

exposing its widgets for manipulation by the

controller.

– In practice, you sometimes don’t quite get here, but this is

the goal.

• Especially if you use this style, you can skip testing

the View altogether.

Page 41: 谷歌 Scott-lessons learned in testability

After Testability Refactoring #3

Page 42: 谷歌 Scott-lessons learned in testability

Presenter Unit Test Using EasyMock

@Testpublic public public public void void void void testOnLogin_unknownUsertestOnLogin_unknownUsertestOnLogin_unknownUsertestOnLogin_unknownUser() {() {() {() { weatherService.expectGetWeatherForUser("unknown"); EasyMock.expect(weatherView.getUserName()).andReturn("unknown"); weatherView.setUnknownUserVisible(true);true);true);true); EasyMock.expectLastCall(); weatherView.setEventHandler(EasyMock.anyObject(WeatherViewEventHandler.classclassclassclass));));));)); EasyMock.expectLastCall();

EasyMock.replay(weatherView);

WeatherHomePresenter presenter = new new new new WeatherHomePresenterWeatherHomePresenterWeatherHomePresenterWeatherHomePresenter((((weatherServiceweatherServiceweatherServiceweatherService, , , , weatherViewweatherViewweatherViewweatherView);););); presenter.onLogin();

EasyMock.verify(weatherView); weatherService.verify();}

This test uses a manually created mock to make handling the async callback easier.

Page 43: 谷歌 Scott-lessons learned in testability

Question

• Why does the View make the Presenter do

this:weatherView.setUnknownUserVisible(true);;;;

• Instead of this:weatherView.getUnknownUser().setVisible(true)

Page 44: 谷歌 Scott-lessons learned in testability

Answer

weatherView.getUnknownUser().setVisible(true)

• Is hard to test because it is hard to mock:

–To mock this, we would have to mock not only the

WeatherView, but also the UnknownUser Label inside

of it.

• The above code is “talking to strangers”.

Page 45: 谷歌 Scott-lessons learned in testability

Law of Demeter

• Also known as the “Don’t talk to strangers” rule.

• It says:

–Only have knowledge of closely collaborating objects.

–Only make calls on immediate friends.

• Look out for long chained “get”-style calls (and don’t do them:

a.getB().getC().getD()

• Your system will be more testable (and maintainable,

because you have to rework calling objects less often).

Page 46: 谷歌 Scott-lessons learned in testability

What’s the Point?• To write good unit tests, we need to be able to insert mocks and fakes

into the code.

• Some things that help us do that:

– Eliminating static methods and singletons.

– Asking for dependencies instead of creating them.

– Using design patterns that promote loose coupling, especially between

business and presentation logic.

– Obeying the Law of Demeter.

• Code that does not do these things will often have poor test coverage.

Page 47: 谷歌 Scott-lessons learned in testability

What Can I Do?• Developers:

– Follow these practices!

• Testers:

– Educate your developers.

– Jump into the code and drive testability improvements.

• A good way to motivate this is to track test coverage metrics.

Page 48: 谷歌 Scott-lessons learned in testability

Questions?Scott McMaster

Google

Kirkland, Washington USA

scott.d.mcmaster (at) gmail.com

Page 49: 谷歌 Scott-lessons learned in testability

Bonus Slides

Page 50: 谷歌 Scott-lessons learned in testability

Model-View-Controller (MVC)

Page 51: 谷歌 Scott-lessons learned in testability

Model-View-Controller (MVC)

• View directly accesses the Model and fires

events to the Controller.

• Controller performs operations on the Model.

• Controller doesn’t really know about the View

other than selecting the View to render.

Page 52: 谷歌 Scott-lessons learned in testability

Which to Use?

• Many web MVC frameworks exist (Struts,

Rails, ASP.NET MVC).

• But these days, we work more with MVP.

Page 53: 谷歌 Scott-lessons learned in testability

Manually Created Mockpublic class public class public class public class MockWeatherServiceAsyncMockWeatherServiceAsyncMockWeatherServiceAsyncMockWeatherServiceAsync implements implements implements implements WeatherServiceAsyncWeatherServiceAsyncWeatherServiceAsyncWeatherServiceAsync { { { {

private private private private List<String> List<String> List<String> List<String> expectGetWeatherForUserCallsexpectGetWeatherForUserCallsexpectGetWeatherForUserCallsexpectGetWeatherForUserCalls = = = = Lists.Lists.Lists.Lists.newArrayListnewArrayListnewArrayListnewArrayList();();();(); private private private private List<String> List<String> List<String> List<String> observeGetWeatherForUserCallsobserveGetWeatherForUserCallsobserveGetWeatherForUserCallsobserveGetWeatherForUserCalls = = = = Lists.Lists.Lists.Lists.newArrayListnewArrayListnewArrayListnewArrayList();();();();

// More @Overrides not shown on the slide.

@Override public public public public void void void void getWeatherForUsergetWeatherForUsergetWeatherForUsergetWeatherForUser(String (String (String (String userNameuserNameuserNameuserName, , , , AsyncCallbackAsyncCallbackAsyncCallbackAsyncCallback<Weather> callback) {<Weather> callback) {<Weather> callback) {<Weather> callback) { observeGetWeatherForUserCalls.add(userName); if if if if ("("("("scottscottscottscott".equals(".equals(".equals(".equals(userNameuserNameuserNameuserName)) {)) {)) {)) { callback.onSuccess(new Weather());new Weather());new Weather());new Weather()); } elseelseelseelse { { { { callback.onSuccess(nullnullnullnull);););); } }

public public public public voidvoidvoidvoid expectGetWeatherForUserexpectGetWeatherForUserexpectGetWeatherForUserexpectGetWeatherForUser((((StringStringStringString userNameuserNameuserNameuserName) {) {) {) { expectGetWeatherForUserCalls.add(userName); }

public public public public voidvoidvoidvoid verifyverifyverifyverify() {() {() {() { assertEquals(expectGetWeatherForUserCalls, observeGetWeatherForUserCalls); expectGetWeatherForUserCalls.clear(); observeGetWeatherForUserCalls.clear(); }}

Page 54: 谷歌 Scott-lessons learned in testability

2012-12-20