Designing For Testabilitycs340ta/winter2019/notes/rodham/18... · • Dependencies created by the...

Post on 14-Oct-2020

2 views 0 download

transcript

Designing For Testability

Characteristics of Testable Code

• Highly cohesive • Loosely coupled

– Dependencies minimized• Dependency injected• Minimal use of static methods and final methods• Static methods contain few parameters• Complex object creation logic is placed in Factories and/or Builders

Features that are Difficult to Automate

• Dependencies created by the class under test– Prevents mocking / unit isolation

• Final methods– Can’t be overridden by mocks or other types of test doubles

• Static methods– Can’t be overridden by mocks or other types of test doubles

• Code that contains complex object creation logic (especially if it creates complex object graphs)

• GUI / View Code

Strategies

• Test-Driven Development• Dependency Injection• Use of configurable factories with simple (preferably no-argument)

constructors• In-class wrapper methods for calls to static method• Use of MVC / MVP with view interfaces

Problem: Dependencies Created by the Class Under Test

public class PayrollProcessor {private DatabaseService dbService;

public PayrollProcessor() {dbService = new DatabaseService();

}

public double getTaxAmount(String employeeID) {Employee emp = dbService.getEmployee(employeeID);

double taxAmount = 0;

// Some complicated code that uses information from// 'emp' to calculate taxes

return taxAmount;}

}

Solution• Dependency Injection• Mocking of the DatabaseService instance in the test

public class PayrollProcessor2 {private DatabaseService dbService;

public PayrollProcessor2(DatabaseService dbService) {this.dbService = dbService;

}

public double getTaxAmount(String employeeID) {Employee emp = dbService.getEmployee(employeeID);

double taxAmount = 0;

// Some complicated code that uses information from 'emp' to // calculate taxes

return taxAmount;}

}

Solution: Setter Injection

public class PayrollProcessor3 {private DatabaseService dbService;

public void setDbService(DatabaseService dbService) {this.dbService = dbService;

}

public double getTaxAmount(String employeeID) {Employee emp = dbService.getEmployee(employeeID);

double taxAmount = 0;

// Some complicated code that uses information from 'emp' to // calculate taxes

return taxAmount;}

}

The “Mock Object” design pattern

• Allows “mock objects” to be inserted anywhere in a program

• Allows a class to be isolated from its dependencies during unit testing, or for other reasons (e.g., a class I depend on doesn’t exist yet, or I want to avoid calling it for some reason)

• Supports “fault injection”– Cause the software to fail at points where it normally wouldn’t to

test error handling code

• Easy to generate with frameworks such as Mockito and EasyMock

• Mock objects can simulate the behavior of complex, real (non-mock) objects and are therefore useful when a real object is impractical or impossible to incorporate into a unit test.

• If an object has any of the following characteristics, it may be useful to use a mock object in its place:– supplies non-deterministic results (e.g., current time or current temperature);– has states that are difficult to create or reproduce (e.g., a network error);– is slow (e.g., a complete database, which would have to be initialized);– does not yet exist or may change behavior;– would have to include information and methods exclusively for testing

purposes (and not for its actual task).• Mock object method implementations can contain assertions of their own. This

means that a true mock, in this sense, will examine the context of each call—perhaps checking the order in which its methods are called, perhaps performing tests on the data passed into the method calls as arguments.

The “Mock Object” design pattern

Mock Object Creation with Mockitopublic class PayrollProcessorTest {

@Testpublic void testGetTaxAmount() {

DatabaseService mockDBService = Mockito.mock(DatabaseService.class);

Employee mockEmployee = Mockito.mock(Employee.class);Mockito.doReturn(mockEmployee).when(

mockDBService.getEmployee(Mockito.anyString()));

// Mockito.when calls to setup mockEmployee to do what we will write // our test to expect

PayrollProcessor2 processor = new PayrollProcessor2(mockDBService);

// Code to test the getTaxAmount() method assuming that the // DatabaseService returns an employee object that does what we mocked // our mockEmployee to do

}}

Dependency Injection• Programming style that allows objects to receive their dependencies

instead of creating them• Makes your design more flexible (dependencies are no longer “hard-

coded” into the class that uses them• Facilitates testing by making it easy for a test to mock a dependency• Choose a “Dependency Injection Container”

– Spring, Ninject, Google Guice, etc.• Configure your dependency injection container with a mapping from

abstract interfaces to concrete classes that implement them• Create objects by asking the dependency injection container for an

implementation of an abstract interface• Allows program code to depend on abstract interfaces, and not on

concrete classes• Allows mock objects to be easily inserted anywhere in the code

Problem: Final Methods

• Mocking frameworks mock interfaces by creating their own implementations

• They mock classes by creating subclasses• Final methods cannot be sub-classed, so most mocking

frameworks cannot mock them• Final methods can be tested, but if the dependency you want to mock

has a final method, you typically cannot replace that method with a mock implementation– Can’t do this:

Mockito.doReturn(xyz).when(mockObject.myFinalMethod())

Solutions

1. Minimize the use of final methods (warning: virtual methods are slower than final methods)

2. Use a mocking framework that can mock final methods (i.e. PowerMock)1. PowerMock is slow2. Generally considered bad practice to use it

Problem: Static Methods

• Similar problems as final methods– Can’t be mocked by most mocking frameworks

• Static methods are testable, but classes that have dependencies on them are difficult to test

Solutions

1. Minimize the use of static methods2. Wrap calls to static methods in non-static methods in the dependent

class3. Use a mocking framework that can mock static methods (i.e.

PowerMock)

Problem: Code that Contains Complex Creation Logic

• Similar to the general problem of classes that create their own dependencies

• Difficult to use dependency injection

Solution

• Use a factory to create the dependency object(s)• Use a non-static ‘get…’ method to create the object

– Makes the entire factory mockable

public class ServiceFactory {public DatabaseService getDatabaseService() {

DatabaseService dbService = null;

// Complex logic to create and initialize a // database service instance.

return dbService;}

}

Factory in Greater Detail

Problem: GUI / View Code

• GUIs are difficult to test using test automation tools• GUIs change frequently (making GUI test code brittle)• GUI automation tools (i.e. Selenium for browser-based applications)

are somewhat flaky• GUI tests that automate and mimic user behavior are slow (due to

screen creation and redraws)

Solution

• Use an MVC / MVP design– Separate actual view code from what can go in controller/presenter

and model classes that are easier to test

• Use interfaces for your views and make controllers/presenters only depend on the view interfaces (not the actual views)– Allows mocking of the view code– Will still need a few system level tests (automated or manual) but

most view code can be tested without actually generating the view