0% found this document useful (0 votes)
134 views288 pages

How To Write Hard To Test Code

How to Write Hard to Test Code

Uploaded by

Thanh Nguyen
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
134 views288 pages

How To Write Hard To Test Code

How to Write Hard to Test Code

Uploaded by

Thanh Nguyen
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 288

Tutorial: How to Write Hard to Test Code

-- Mi!ko Hevery & Cory Smith


Copyright is held by the author/owner(s). OOPSLA'09, October 2529, 2009, Orlando, FL, USA. ACM 09/10.

Agenda
Psychology of Testing Work in Constructor Law of Demeter Inheritance BREAK Global State (Cory Smith) Code Review

Testing is not like frosting

Development Model
Software Engineers

QA

Test Engineers

Development Model
Software Engineers

Develop

QA

Test Engineers

Development Model
Software Engineers

Develop

QA

Manual Tests Exploratory Tests

Test Engineers

Development Model
Software Engineers

Develop

QA

Manual Tests Exploratory Tests

Test Engineers

Automated Tests

Development Model
Software Engineers

Develop

QA

Manual Tests Exploratory Tests

Test Engineers

Automated Tests

Tools

Testing magic

Development Model
Software Engineers

Develop

QA

Manual Tests Exploratory Tests

Test Engineers

Automated Tests

Tools

Testing magic

Development Model
Software Engineers

Develop

Design

Testing magic

QA

Manual Tests Exploratory Tests

Test Engineers

Automated Tests

Tools

Testing magic

Micro Development Model

Micro Development Model


Develop

Micro Development Model


Develop

Test

Micro Development Model


Develop

Test

Micro Development Model


Develop Check-In Test

Micro Development Model


Develop Check-In Test

Micro Development Model


Develop Check-In Test

Micro Development Model


Develop Check-In Test

automate

Excuses

No Tests

Excuses
Valid Excuse

No Tests

Excuses
Valid Excuse

No Tests

Common Misconception

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

No Tests

Common Misconception

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

Dirties Design

No Tests

Common Misconception

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

Dirties Design

No Tests

It

e do

c t n

b h tc

s g u

Common Misconception

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

Dirties Design

No Tests

It

Common Misconception

Its

slo

e do

we r

c t n

b h tc

s g u

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

Dirties Design

No Tests

It

Common Misconception

Its

slo

e do

we r

c t n

b h tc

s g u

Its boring

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

Dirties Design

No Tests
Ha

we r

It

slo

e do

c t n

b h tc

s g u

Its boring

rd to ch an ge

Common Misconception

Its

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

Dirties Design

No Tests
To o ma ny int erf

we r

It

slo

e do

c t n

atc

hb

s g u

Ha rd to ch an ge

ac

Its boring

es

Common Misconception

Its

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

Dirties Design

No Tests
Ha

Testing is for QA

To o
hb s g u
we r slo
Its boring

ma

ny

int

erf

It

e do

c t n

atc

ac

es

rd to ch an ge

Common Misconception

Its

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase
ri w I

I U te

Dirties Design

No Tests
Ha

Testing is for QA

To o
hb s g u
we r slo
Its boring

ma

ny

int

erf

It

e do

c t n

atc

ac

es

rd to ch an ge

Common Misconception

Its

Excuses
Valid Excuse

Leg

acy

cod

e-b

ase

Dont know how


ri w I

I U te

Dirties Design

No Tests
Ha

Testing is for QA

To o
hb s g u
we r slo
Its boring

ma

ny

int

erf

It

e do

c t n

atc

ac

es

rd to ch an ge

Common Misconception

Its

How do you write HARD TO TEST code?

Bugs

Bugs

Bugs

Bugs

Bugs

Bugs

Cost of Fixing the Bug


Probability Difculty of of Finding Finding the a Bug Bug Logical Bugs Wiring Bugs Rendering Bugs HIGH HARD Difculty of Fixing the Bug HARD

MEDIUM

MEDIUM

MEDIUM

MEDIUM

EASY

EASY

But my code is different!

I have a Super-Bug!

Testing is like industrial revolution

Different Kinds of Testing

Different Kinds of Testing

Unit

Different Kinds of Testing

Unit

(if)Test individual classes/methods in isolation (~1ms)

Different Kinds of Testing

Functional

Unit

(if)Test individual classes/methods in isolation (~1ms)

Different Kinds of Testing


(Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Functional

Unit

Different Kinds of Testing


Endto-End (Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Functional

Unit

Different Kinds of Testing


Endto-End (Configuration) Test the whole system pretending to be user (>10 secs) (Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Functional

Unit

Different Kinds of Testing


Endto-End (Configuration) Test the whole system pretending to be user (>10 secs) (Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Execution Time Flakiness/Debugging/Maintenance Cost

Functional

Unit

Different Kinds of Testing


Endto-End (Configuration) Test the whole system pretending to be user (>10 secs) (Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Execution Time Flakiness/Debugging/Maintenance Cost

Functional

Unit
# of Tests

Different Kinds of Testing


Endto-End (Configuration) Test the whole system pretending to be user (>10 secs) (Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Execution Time Flakiness/Debugging/Maintenance Cost

Software Engineers

Functional

Unit
# of Tests

Different Kinds of Testing


Software Engineers Test Engineers
Endto-End (Configuration) Test the whole system pretending to be user (>10 secs) (Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Execution Time Flakiness/Debugging/Maintenance Cost

Functional

Unit
# of Tests

Different Kinds of Testing


Software Engineers Test Engineers
Endto-End (Configuration) Test the whole system pretending to be user (>10 secs) (Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Execution Time Flakiness/Debugging/Maintenance Cost

Functional

Unit
# of Tests

Different Kinds of Testing


Software Engineers Test Engineers
Endto-End (Configuration) Test the whole system pretending to be user (>10 secs) (Wiring) Test interaction/contracts between classes (<1 sec) (if)Test individual classes/methods in isolation (~1ms)

Execution Time Flakiness/Debugging/Maintenance Cost

Functional

Unit
# of Tests

Dev Model Revisited

Develop

CI-build Check-In

Test

Review

Dev Model Revisited


Education

Develop

CI-build Check-In

Test

Review

Dev Model Revisited


Education

Develop

CI-build Check-In

Test

Review

Tools

Dev Model Revisited


Education

Develop

CI-build Check-In

Test

Review

Tools

Visibility

Dev Model Revisited


Education Enforcement

Develop

CI-build Check-In

Test

Review

Tools

Visibility

Dev Model Revisited


Education Enforcement

Testing on the Toilet Tech Talks / Blogs 1 on 1 Training Mercenaries Develop Immersion

CI-build Check-In

Test

Review

Tools

Visibility

Dev Model Revisited


Education Enforcement

Testing on the Toilet Tech Talks / Blogs 1 on 1 Training Mercenaries Develop Immersion

CI-build Check-In

Test

Review

Tools Analysis Reports


Tools Visibility

Dev Model Revisited


Education Enforcement

Testing on the Toilet Tech Talks / Blogs 1 on 1 Training Mercenaries Develop Immersion

CI-build Check-In

Test

Review

Tools Analysis Reports


Tools

Games Dashboards Scoreboards Trend Graphs

Visibility

Dev Model Revisited


Education Enforcement

Testing on the Toilet Tech Talks / Blogs 1 on 1 Training Mercenaries Develop Immersion

RoboCop Testability Coverage


CI-build Check-In

Test

Review

Tools Analysis Reports


Tools

Games Dashboards Scoreboards Trend Graphs

Visibility

Education: Test Mercenaries


Internal consulting group
specializing in testability refactorings and inuencing developers for 3-6 months

2-3 mercenaries join the group

Education: Tech Talks


Weekly series Focus on common
singletons, LoD

mistakes which make code hard to test

new, global-state, Many available on You Tube

Education:
We try to stick it on everyones
monitor

Set of red-ags to look for URL pointing to an explanation of


the red-ag

The reviewer can point the author


to the explanation page

Education: Mission Impossible


Mercenaries in reverse Total Immersion work with team for a
month which gets it

Very good feedback

Education: TotT
Weekly publications Testing Tips (not
concepts)

Captive audience Very successful

Education: Blog
Venue for larger concepts which do not t
to TotT.

Shared with outside world

Education: TDD University


Build small project from scratch Learn how the code should be like Rewrite existing piece of code To see how it is different Refactor existing code To learn how to get from here to
there

Tools: Coverage
Simple but effective Just having it Squeaky wheel gets
the grease installed makes people thing about improving coverage

Tools: Test Farms


Farms Ability to run massive
test suites in parallel

Selenium unit-test browser matrix

Tools: Testability Report


Code coverage
for testability

Get high level

view about how testable the code is

Tools: Testability Advice


Personal
into IDE buddy testability advisor

Plans to integrate Virtual pairing

Visibility: Games

Make it into a game

Visibility: Bubbles

See quality of check-ins over time Time, Tests, User, Size

Visibility: Test Pareto

Test ratios per developer Impact per developer

Visibility: Test Trend

Test focus over time

Visibility: Certication
Well dened standards To be at N level you need to
do X,Y, and Z.

TC level prestige

Enforcement: Tough Love


Remove manual testing resources Provide knowledge and support

Enforcement: RoboCop
Coverage can not get worse with
any one CL any one CL

Testability can not get worse with Plans for arbitrary rule
enforcement

Progress
Too many tests too run CI standard Everyone agrees tests are good idea TC Level Dependency Injection - GUICE Ratio System vs Unit is improving

Work in Constructor

Unit Testing a Class

Unit Testing a Class

Test Driver

Unit Testing a Class

Test Driver

Class Under Test

Unit Testing a Class

Stimulus
Test Driver Class Under Test

Unit Testing a Class

Stimulus
Test Driver Class Under Test

Asserts

Unit Testing a Class

Test Driver

Class Under Test

Unit Testing a Class

Test Driver

Class Under Test

Unit Testing a Class

Test Driver

Class Under Test

Unit Testing a Class

Test Driver

Class Under Test

Other Class

Unit Testing a Class

Test Driver

Class Under Test

Other Class

Other Class

Unit Testing a Class

Test Driver

Class Under Test

Other Class

Other Class

Object Lifetime and Calling Object Instantiated Object Passed In Global Object

Other Class

Unit Testing a Class


File System Other Class CPU Intensive Other Class

Test Driver

Class Under Test

Other Class
Destructive operation

Other Class Other Class

Object Lifetime and Calling Object Instantiated Object Passed In Global Object

Other Class

Other Servers

Unit Testing a Class


File System Other Class CPU Intensive Other Class

Test Driver

Class Under Test

Other Class
Destructive operation

Other Class Other Class

Object Lifetime and Calling Object Instantiated Object Passed In Global Object

Other Class

Other Servers

Unit Testing a Class


File System Other Class CPU Intensive Other Friendly Class

Test Driver

Class Under Test

Other Class
Destructive operation

Other Friendly Class Other Class

Object Lifetime and Calling Object Instantiated Object Passed In Global Object

Other Friendly Class

Other Servers

Unit Testing a Class


File System Other Class CPU Intensive Other Friendly Class

Seam

Test Driver

Class Under Test

Other Class
Destructive operation

Other Friendly Class Other Class

Object Lifetime and Calling Object Instantiated Object Passed In Global Object

Other Friendly Class

Other Servers

Object Graph Construction & Lookup

Business Logic

Object Graph Construction & Lookup

Business Logic

Unit Testing a Class


Friendly Test Driver Class Under Test

Seam

Friendly

Friendly

Object Lifetime and Calling Object Instantiated Object Passed In Global Object

Unit Testing a Class


Friendly Test Driver Class Under Test

Seam

Friendly

Friendly

Object Lifetime and Calling Object Instantiated Object Passed In Global Object

How Do You Write Hard To


Test Code?

How Do You Write Hard To


Test Code?

You mix object creation code


with business logic. This will assure that a test can never construct a graph of objects different from production. Hence nothing can be tested in isolation.

Cost of Construction
To test a method you rst need to
instantiate an object:

Work inside of constructor has no seams Cant override Your test must successfully navigate the
constructor maze

Do as little work in constructor as possible

Cost of Construction
class Document { String html; Document(String url) { HtmlClient client = new HtmlClient(); html = client.get(url); } }

Cost of Construction
class Document { String html; Mixing graph construction with work

Document(String url) { HtmlClient client = new HtmlClient(); html = client.get(url); } }

Cost of Construction
class Document { String html; Mixing graph construction with work

Document(String url) { HtmlClient client = new HtmlClient(); html = client.get(url); } } Doing work in constructor

Cost of Construction
class Document { String html; Document(HtmlClient client, String url) { html = client.get(url); } }

Cost of Construction
class Document { String html; Document(HtmlClient client, String url) { html = client.get(url); } } Doing work in constructor

Cost of Construction
class Document { String html; Document does not care about client, it cares about what client can produce

Document(HtmlClient client, String url) { html = client.get(url); } } Doing work in constructor

Cost of Construction
class Document { String html; Document does not care about client, it cares about what client can produce

Document(HtmlClient client, String url) { html = client.get(url); } } Doing work in constructor


Law of the Demeter: Asking for something you dont need directly only to get to what you really want.

Cost of Construction
class Car { Engine engine; Car(String modelNo) { EngineFactory factory = new EngineFactory(); engine = factory.get(modelNo); } }

Cost of Construction
class Car { Engine engine;
The fact that a Car knows about EngineFactory Seems silly. But somehow if car is a Document it is ok for Document to be its own factory.

Car(String modelNo) { EngineFactory factory = new EngineFactory(); engine = factory.get(modelNo); } }

Cost of Construction

Cost of Construction
class Document { String html; Document(String html) { this.html = html; } }

Cost of Construction
class Document { String html; Document(String html) { this.html = html; } }

class DocumentFactory { HtmlClient client; DocumentFactory(HtmlClient client) { this.client = client; } Document build(String url) { return new Document(client.get(url)); }

Cost of Construction
class Document { String html; Document(String html) { this.html = html; } class Printer { void print(Document html) { // do some work here. } } }

class DocumentFactory { HtmlClient client; DocumentFactory(HtmlClient client) { this.client = client; } Document build(String url) { return new Document(client.get(url)); }

Cost of Construction
class Document { String html; class Printer { void print(Document html) { // do some work here. } Document(String html) { this.html = html; } } } Easy to test since Document is easy to construct

class DocumentFactory { HtmlClient client; DocumentFactory(HtmlClient client) { this.client = client; } Document build(String url) { return new Document(client.get(url)); }

Cost of Construction
Test has to successfully navigate the Objects require construction often

constructor each time instance is needed indirectly making hard to construct objects a real pain to test with

Wiring

Composition vs Inheritance

The purpose of Inheritance is polymorphic


behavior

If you dont take advantage of

polymorphism you should reuse code through delegation / composition

Composition vs Inheritance
Servlet LoggingServlet DbTransactionServlet NoUserServlet UserLoggedInServlet NormalUserPage WelcomePage MainPage AdminUserPage SettingsPage

Composition vs Inheritance
class SettingsPageTest extends TestCase { public void testAddUser() { SettingsPage p = new SettingsPage(); // What about Logging? // What about Database? // What about User Verification? // What about Admin User Verification? // How do I inject mocks into this? HttpServletRequest req = ....? HttpServletResponse res = ...? // What parameters => Add User Action? p.doGet(req, resp); // What do I assert? // This test is not unit test! // Failed test => No clue why! } }

Servlet LoggingServlet DbTransactionServlet UserLoggedInServlet AdminUserPage SettingsPage

Composition vs Inheritance

With composition at test time we can

build different object graphs under tests.

With inheritance at test time we can not


build different object inheritance!

Composition vs Inheritance
Servlet LoggingServlet DbTransactionServlet UserLoggedInServlet AdminUserPage SettingsPage

Composition vs Inheritance
Servlet LoggingServlet DbTransactionServlet UserLoggedInServlet AdminUserPage SettingsPage

Composition vs Inheritance
Servlet LoggingServlet DbTransactionServlet UserLoggedInServlet AdminUserPage SettingsPage

public void testAddUser() { SettingsPage p = new SettingsPage(); HttpServletRequest req = ....? HttpServletResponse res = ...? // What parameters => Add User Action? p.doGet(req, resp); // What do I assert? }

Composition vs Inheritance
Servlet LoggingServlet DbTransactionServlet UserLoggedInServlet AdminUserPage SettingsPage

public void testAddUser() { SettingsPage p = new SettingsPage(); HttpServletRequest req = ....? HttpServletResponse res = ...? // What parameters => Add User Action? p.doGet(req, resp); // What do I assert? } public void testAddUser() { UserRepository users = new InMemoryUserRepository(); SettingsPage page = new SettingsPage(users); page.addUser(jon); assertNotNull(users.getUser(jon)) }

Composition vs Inheritance
Servlet LoggingServlet DbTransactionServlet UserLoggedInServlet AdminUserPage SettingsPage Factory public void testAddUser() { SettingsPage p = new SettingsPage(); HttpServletRequest req = ....? HttpServletResponse res = ...? // What parameters => Add User Action? p.doGet(req, resp); // What do I assert? } public void testAddUser() { UserRepository users = new InMemoryUserRepository(); SettingsPage page = new SettingsPage(users); page.addUser(jon); assertNotNull(users.getUser(jon)) }

Composition vs Inheritance

There are no seams in the inheritance

hierarchy. It is all or nothing proposition, which makes Unit Testing impossible.

Composition vs Inheritance

Sub classing in tests


Anonymous inner subclass and override is
the ultimate in swiss army knife of testing.

It is a code-smell Subclassing for tests, begs for whatever you


are subclassing to live in a different object. So that in test you can replace that portion with friendly

Sub classing in tests


class LoginPage { public void login(String user, String password){ User user = loadUser(user); if (!user.getPassword.equals(password)) { throw new InvalidPassword(); } } // protected for test access protected User loadUser(String user) { ... } } testLogin() { final User u = new User(joe, pwd); LoginPage lp = new LoginPage() { protected User loadUser(String user) { return u; } } lp.login(joe, pwd); }

Sub classing in tests


class LoginPage { LoginPage(UserRepo userRepo){...} public void login(String user, String password){ User user = userRepo.getUserByName(user); if (!user.getPassword.equals(password)) { throw new InvalidPassword(); } } } testLogin() { User u = new User(joe, pwd); UserRepo repo = new InMemoryUserRepo() repo.addUser(u); LoginPage lp = new LoginPage(repo); lp.login(joe, pwd); }

Sub classing in tests

Law of Demeter

Law of Demeter
Imagine your are in a store and the item
you are purchasing is $25.

Law of Demeter
Imagine your are in a store and the item
you are purchasing is $25.

Do you give the clerk $25?

Law of Demeter
Imagine your are in a store and the item
you are purchasing is $25.

Do you give the clerk $25? Or do you give the clerk your wallet and
let him retrieve the $25?

Law of Demeter
class Goods { AccountsReceivable ar; void purchase(Customer c) { Money m = c.getWallet().getMoney(); ar.recordSale(this, m); } }

Law of Demeter
class Goods { AccountsReceivable ar; void purchase(Customer c) { Money m = c.getWallet().getMoney(); ar.recordSale(this, m); } To test this we }

need to create a valid Customer with a valid Wallet which contains the real item of interest. (Money)

Law of Demeter
class GoodsTest { void testPurchase() { AccountsReceivable ar = new MockAR(); Goods g = new Goods(ar); Money m = new Money(25, USD); Wallet v = new Wallet(m); Customer c = new Customer(v); g.purchase(c); assertEquasl(25, ar.getSales()); } }

Law of Demeter
class Goods { AccountsReceivable ar; void purchase(Money m) { ar.recordSale(this, m); } } class GoodsTest { void testPurchase() { AccountsReceivable ar = new MockAR(); Goods g = new Goods(ar); g.purchase(new Money(25, USD)); assertEquasl(25, ar.getSales()); } }

Law of Demeter

Law of Demeter
You only ask for objects which you directly
need (operate on)

Law of Demeter
You only ask for objects which you directly
need (operate on)

a.getX().getY().... is a dead givaway

Law of Demeter
You only ask for objects which you directly
need (operate on)

a.getX().getY().... is a dead givaway serviceLocator.getService() is breaking the


Law of Demeter

BREAK (30 minutes)

Global State

Lets test a class


class A { private HardDisk d; public A() { d = new HardDisk(); } public void doWork() { d.open(); d.format(FAT); d.close(); } }

We cant test this


class A { private HardDisk d; public A() { d = new HardDisk(); } public void doWork() { d.open(); d.format(FAT); d.close(); } }

There is no way to

intercept the calls to HardDisk this is to actually format your hard disk and have a post condition

The only way to test

There is only one HardDisk


class B { private HardDisk d; public B() { d = HardDisk.get(); } public void doWork() { d.open(); d.format(FAT); d.close(); } }

Since there is only one


cant intercept calls to HardDisk

HardDisk we might need to enforce Singleton-ess

Still cant test this as we

Lets try to write a test


testBFormatsHD() { HardDisk.init(/temp); class B { new B().doWork(); private HardDisk d; assert???(...); public B() { d = HardDisk.get(); } } testBFormatsHD2() { // we forgot to init public void doWork() { // guess the default is d.open(); // /dev/hd0 :-) d.format(FAT); d.close(); new B().doWork(); } // Your machine is gone } }

Calling init() multiple times on a Singleton is a bit weird

Congurable Singleton
class B { private HardDisk d; testBFormatsHD() { public B() { MockHD hd = new MockHD(); d = HardDisk.get(); HardDisk.set(hd); } new C().doWork(); public void doWork() { d.open(); assertTrue(hd.verify()); d.format(FAT); } d.close(); } }

Its not a singleton if you can change it!

What about Service Locator


To test our singleton we need Ability to change the instance Ability to congure the singleton multiple times Both of the above go against what a singleton is Maybe Service Locator / Registry is better

We need to intercept the HD


class C { private HardDisk d; public C() { d = ServiceLocator.get(HardDisk.class); } public void doWork() { d.open(); d.format(FAT); d.close(); } }

Now we can intercept the calls


class C { private HardDisk d; testCFormatsHD() { public C() { MockHD hd = new MockHD(); d = ServiceLocator ServiceLocator .get(HardDisk.class); .registerHardDisk(hd); } public void doWork() { d.open(); d.format(FAT); d.close(); } } new C().doWork(); assertTrue(hd.verify()); }

There is something weird


testCFormatsHD() { MockHD hd = new MockHD(); ServiceLocator .RegisterHardDisk(hd); new C().doWork(); assertTrue(hd.verify()); }

Red and green areas


share no common variables!

How do they talk? Are they related? Does order matter? What if the red is in
setUp() in superclass?

API lies
Class API hides true dependencies Constructor mentions nothing of HardDisk Methods mention nothing of HardDisk Attempting to use class C outside of HardDisk
will fail.

Attempting to reuse the component in a different


project will fail because it will drag dependencies with it.

Dependencies are there but are HIDDEN!

Making the dependencies explicit


class D { private HardDisk hd; public D(HardDisk hd) { this.hd = hd; } public void doWork() { hd.open(); hd.format(FAT); hd.close(); } }

Testing explicit dependencies is easier


class D { private HardDisk d; public D(HardDisk d) { this.d = d; } public void doWork() { d.open(); d.format(FAT); d.close(); } }

testDFormatsHD() { MockHD hd = new MockHD(); new D(hd).doWork(); assertTrue(hd.verify()); }

Dependencies are EXPLICIT!

Explicit Dependencies
Also known as: Dependency Injection Inversion of Control Make the order of initialization clear Dont pretend to be cleaner then they are WY(Declare)IWY(Need) -- No hiding

Difculty of Testing
Construction
public void doWork() { d.open(); d.format(FAT); d.close(); } } class A { private HardDisk d; public A() { d = new HardDisk(); } class B { private HardDisk d; public B() { d = HardDisk.get(); } public void doWork() { d.open(); d.format(FAT); d.close(); } }

Singleton

class C { private HardDisk d; public C() { d = ServiceLocator .get(HardDisk.class); } public void doWork() { d.open(); d.format(FAT); d.close(); } }

Service

class D { private HardDisk d; public D(HardDisk d) { this.d = d; } public void doWork() { d.open(); d.format(FAT); d.close(); } }

Injection

Testable == Understandable
Easy to Test --> Easy to Understand Hard to Test --> Hard to Understand

Why?

What is Testability?

A testability is directly proportional to the number of locations where we can intercept the normal ow of the code

Interception == Polymorphism

Would that mean that static methods are harder to tests then instance methods because they can not be intercepted?

Interception possibilities
class A { class D { private HardDisk d; private HardDisk d; public A() { public D(HardDisk d) { d = new HardDisk(); this.d = d; } } public void doWork() { d.open(); d.format(FAT); d.close(); } } } public void doWork() { d.open(); d.format(FAT); d.close(); }

Constructors are static


Static constructor prevents interception Statically constructed object is equivalent to nonintercept-able objects

Why? Because interception involves sub-classing / alternate-implementation

Interception possibilities
class A { class D { private HardDisk d; private HardDisk d; public A() { public D(HardDisk d) { d = new HardDisk(); this.d = d; } } public void doWork() { d.open(); d.format(FAT); d.close(); } } } public void doWork() { d.open(); d.format(FAT); d.close(); }

Service Locator
So if new is static (prevents interception) THEN ServiceLocator is good as it allows easy object
substitution and interception

True BUT Hides dependencies

Service Locator
aka Context Better then a Singleton If you had static look up of services this Hides true dependencies

is an improvement. It is testable but it is not pretty

Service Locator
class House { Door door; Window window; Roof roof; House(Locator locator){ door = locator.getDoor(); window = to locator.getWindow(); What needs be mocked out in test? roof = locator.getRoof(); } }

Service Locator
class House { Door door; Window window; Roof roof; House(Locator locator){ door = locator.getDoor(); window = locator.getWindow(); roof = locator.getRoof(); } }

Service Locator
class House { Door door; Window window; Roof roof;
The API lies about its true dependencies. Only after examining or running the code can we determine what is actually needed

House(Locator locator){ door = locator.getDoor(); window = locator.getWindow(); roof = locator.getRoof(); } }

Service Locator
class House { Door door; Window window; Roof roof; House(Door d, Window w, Roof r){ door = d; window = w; roof = r; } }

Service Locator
class HouseTest { pulic void testServiceLocator() { Door d = new Door(...); Roof r = new Roof(...); Window w = new Window(...); House h = new House(d, r, w); } }

Service Locator
Mixing Responsibilities Lookup Factory Need to have an interface for testing Anything which depends on Service

Locator now depends on everything else.

Global State aka Singletons

API that lies about what it needs Spooky action at a distance

Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

At the end of the month I got my Statement!

Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

At the end of the month I got my Statement! I was out $100!

Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

At the end of the month I got my Statement! I was out $100! Spooky action at a distance!

Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

At the end of the month I got my Statement! I was out $100! Spooky action at a distance! It never passed in isolation

Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }
java.lang.NullPointerException ! at talk3.CreditCard.charge(CreditCard.java:48)

Deceptive API
testCharge() { CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

Deceptive API
testCharge() { CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }
java.lang.NullPointerException ! at talk3.CreditCartProcessor.init(CreditCardProcessor.java:146)

Deceptive API
testCharge() { OfflineQueue.start(); CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

Deceptive API
testCharge() { OfflineQueue.start(); CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }
java.lang.NullPointerException ! at talk3.OfflineQueue.start(OfflineQueue.java:16)

Deceptive API
testCharge() { Database.connect(...); OfflineQueue.start(); CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

Deceptive API
testCharge() { Database.connect(...); OfflineQueue.start(); CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

CreditCard API lies It pretends to not need the CreditCardProcessor


even thought in reality it does.

Deceptive API
testCharge() { Database.connect(...); OfflineQueue.start(); CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

CreditCard API lies It pretends to not need the CreditCardProcessor


even thought in reality it does.

Deceptive API
testCharge() { Database.connect(...); OfflineQueue.start(); CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

CreditCard API lies It pretends to not need the CreditCardProcessor


even thought in reality it does.

Deceptive API
testCharge() { Database.connect(...); OfflineQueue.start(); CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }

CreditCard API lies It pretends to not need the CreditCardProcessor


even thought in reality it does.

Better API
testCharge() {

CreditCard cc; cc = new CreditCard(12..34, ccProc); cc.charge(100); }

Better API
testCharge() { ccProc = new CCProcessor(queue); CreditCard cc; cc = new CreditCard(12..34, ccProc); cc.charge(100); }

Better API
testCharge() { queue = new OfflineQueue(db); ccProc = new CCProcessor(queue); CreditCard cc; cc = new CreditCard(12..34, ccProc); cc.charge(100); }

Better API
testCharge() { db = new Database(...); queue = new OfflineQueue(db); ccProc = new CCProcessor(queue); CreditCard cc; cc = new CreditCard(12..34, ccProc); cc.charge(100); }

Better API
testCharge() { db = new Database(...); queue = new OfflineQueue(db); ccProc = new CCProcessor(queue); CreditCard cc; cc = new CreditCard(12..34, ccProc); cc.charge(100); }

Dependency injection enforces the order of


initialization at compile time.

Code Review

My advice to you...

My advice to you...
Global state is evil! Stay away at all costs!

My advice to you...
Global state is evil! Stay away at all costs! new operator is like kryptonite! Handle with extreme care!

Building a house
class House { Kitchen kitchen = new Kitchen(); Bedroom bedroom; House() { bedroom = new Bedroom(); } // ... }

Building a house
class House { Kitchen kitchen = new Kitchen(); Bedroom bedroom; House() { bedroom = new Bedroom(); } // ... } class HouseTest extends TestCase { public void testThisIsReallyHard() { House house = new House(); // Darn! I'm stuck with those // Kitchen and Bedroom objects // created in the constructor. } }

Building a house
! Flaw: inline object instantiation where fields are declared has the same problems that work in the constructor has.

! Flaw: this may be easy to instantiate but if Kitchen represents something expensive such as file/database access it is not very testable since we could never replace the Kitchen or Bedroom with a testdouble. ! Flaw: Your design is more brittle, because you can never polymorphically replace the behavior of the kitchen or bedroom in the House.

Building a house
class House { Kitchen kitchen; Bedroom bedroom; // Have Guice create the objects and pass them in @Inject House(Kitchen k, Bedroom b) { kitchen = k; bedroom = b; } // ... }

Building a house
class House { Kitchen kitchen; Bedroom bedroom; // Have Guice create the objects and pass them in @Inject House(Kitchen k, Bedroom b) { kitchen = k; bedroom = b; } // ... }

public void testThisIsEasyAndFlexible() { Kitchen dummyKitchen = new DummyKitchen(); Bedroom dummyBedroom = new DummyBedroom(); !House house = new House(dummyKitchen, dummyBedroom); // Awesome, I can use test doubles that are lighter weight. }

Joe the Gardener


class Garden { Garden(Gardener joe) { joe.setWorkday(new TwelveHourWorkday()); joe.setBoots(new ExpensiveBoots()); this.joe = joe; } // ... }

Joe the Gardener


class Garden { Garden(Gardener joe) { joe.setWorkday(new TwelveHourWorkday()); joe.setBoots(new ExpensiveBoots()); this.joe = joe; } // ... } public void testMustUseFullFledgedGardener() { Gardener gardener = new Gardener(); Garden garden = new Garden(gardener); new AphidPlague(garden).infect(); garden.notifyGardenerSickShrubbery(); !assertTrue(gardener.isWorking()); }

Joe the Gardener


! Flaw: The Garden needs a Gardener, but it should not be the responsibility of the Garden to configure the gardener.

! Flaw: In a unit test for Garden the workday is set specifically in the constructor, thus forcing us to have Joe work a 12 hour workday. Forced dependencies like this can cause tests to run slow. In unit tests, youll want to pass in a shorter workday. ! Flaw: You cant change the boots. You will likely want to use a test-double for boots to avoid the problems with loading and using ExpensiveBoots.

Joe the Gardener


class Garden { Gardener joe; @Inject Garden(Gardener joe) { this.joe = joe; } } @Provides Gardener getGardenerJoe( Workday workday, ExpensiveBoots boots) { Gardener joe = new Gardener(); !joe.setWorkday(workday); joe.setBoots(boots); return joe; }

Joe the Gardener


class Garden { Gardener joe; @Inject Garden(Gardener joe) { this.joe = joe; } } @Provides Gardener getGardenerJoe( Workday workday, ExpensiveBoots boots) { Gardener joe = new Gardener(); !joe.setWorkday(workday); joe.setBoots(boots); return joe; }

public void testUsesGardenerWithDummies() { Gardener gardener = new Gardener(); gardener.setWorkday(new OneMinuteWorkday()); gardener.setBoots(null); Garden garden = new Garden(gardener); new AphidPlague(garden).infect(); garden.notifyGardenerSickShrubbery(); !assertTrue(gardener.isWorking()); }

Accounting 101...
class AccountView { ! User user; ! AccountView() { !! user = RPCClient.getInstance().getUser(); ! } }

Accounting 101...
class AccountView { ! User user; ! AccountView() { !! user = RPCClient.getInstance().getUser(); ! } }

public void testUnfortunatelyWithRealRPC() { AccountView view = new AccountView(); // Shucks! We just had to connect to a real // RPCClient. This test is now slow. // ... }

Accounting 101...
! Flaw: We cannot easily intercept the call RPCClient.getInstance () to return a mock RPCClient for testing. (Static methods are noninterceptable, and non-mockable).

! Flaw: Why do we have to mock out RPCClient for testing if the class under test does not need RPCClient?(AccountView doesnt persist the rpc instance in a field.) We only need to persist the User. ! Flaw: Every test which needs to construct class AccountView will have to deal with the above points. Even if we solve the issues for one test, we dont want to solve them again in other tests. For example AccountServlet may need AccountView. Hence in AccountServlet we will have to successfully navigate the constructor.

Accounting 101...
class AccountView { ! User user; @Inject ! AccountView(User user) { ! this.user = user; } } @Provides User getUser(RPCClient rpcClient) { return rpcClient.getUser(); } @Provides @Singleton RPCClient getRPCClient() { return new RPCClient(); }

Accounting 101...
class AccountView { ! User user; @Inject ! AccountView(User user) { ! this.user = user; } } @Provides User getUser(RPCClient rpcClient) { return rpcClient.getUser(); } @Provides @Singleton RPCClient getRPCClient() { return new RPCClient(); }

public void testLightweightAndFlexible() { User user = new DummyUser(); AccountView view = new AccountView(user); // Easy to test and fast with test-double user. }

Supper Car...
class Car { Engine engine; Car(File file) { String model = readEngineModel(file); engine = new EngineFactory().create(model); } // ... }

Supper Car...
class Car { Engine engine; Car(File file) { String model = readEngineModel(file); engine = new EngineFactory().create(model); } // ... } public void testNoSeamForFakeEngine() { // Aggh! I hate using files in unit tests File file = new File("engine.config"); Car car = new Car(file); // I want to test with a fake engine // but I can't since the EngineFactory // only knows how to make real engines. }

Supper Car...
! Flaw: Passing in a file, when all that is ultimately needed is an Engine. ! Flaw: Creating a third party object (EngineFactory) and paying any assorted costs in this non-injectable and non-overridable creation. This makes your code more brittle because you cant change the factory, you cant decide to start caching them, and you cant prevent it from running when a new Car is created. ! Flaw: Its silly for the car to know how to build an EngineFactory, which then knows how to build an engine. (Somehow when these objects are more abstract we tend to not realize were making this mistake). ! Flaw: Every test which needs to construct class Car will have to deal with the above points. Even if we solve the issues for one test, we dont want to solve them again in other tests. For example another test for a Garage may need a Car. Hence in Garage test I will have to successfully navigate the Car constructor. And I will be forced to create a new EngineFactory. ! Flaw: Every test will need a access a file when the Car constructor is called. This is slow, and prevents test from being true unit tests.

Supper Car...
class Car { Engine engine; @Inject Car(Engine engine) { this.engine = engine; } } @Provides Engine getEngine( EngineFactory engineFactory, @EngineModel String model) { return engineFactory.create(model); }

Supper Car...
class Car { Engine engine; @Inject Car(Engine engine) { this.engine = engine; } } @Provides Engine getEngine( EngineFactory engineFactory, @EngineModel String model) { return engineFactory.create(model); }

public void testShowsWeHaveCleanDesign() { Engine fakeEngine = new FakeEngine(); Car car = new Car(fakeEngine); // Now testing is easy, with the car taking // exactly what it needs. }

Command line ags...


class PingServer { Socket socket; PingServer() { socket = new Socket(FLAG_PORT.get()); } }

Command line ags...


class PingServer { Socket socket; PingServer() { socket = new Socket(FLAG_PORT.get()); } }

class PingServerTest extends TestCase { public void testWithDefaultPort() { PingServer server = new PingServer(); // This looks innocent enough, but really // it forces you to mutate global state // (the flag) to run on another port. } }

Command line ags...


! Flaw: In your test you will have to rely on global variable FLAG_PORT in order to instantiate the class. This will make your tests flaky as the order of tests matters.

! Flaw: Depending on a statically accessed flag value prevents you from running tests in parallel. Because parallel running test could change the flag value at the same time, causing failures. ! Flaw: If the socket needed additional configuration (i.e. calling setSoTimeout()), that cant happen because the object construction happens in the wrong place. Socket is created inside the PingServer, which is backwards. It needs to happen externally, in something whose sole responsibility is object graph construction i.e. a Guice provider.

Command line ags...


class PingServer { Socket socket; @Inject PingServer(Socket socket) { this.socket = socket; } } new FlagBinder(binder() .bind(FlagsClassX.class)); @Provides Socket getSocket( @Named("port") int port) { return new Socket(port); }

Command line ags...


class PingServer { Socket socket; @Inject PingServer(Socket socket) { this.socket = socket; } } new FlagBinder(binder() .bind(FlagsClassX.class)); @Provides Socket getSocket( @Named("port") int port) { return new Socket(port); }

class PingServerTest extends TestCase { public void testWithNewPort() { int customPort = 1234; Socket socket = new Socket(customPort); PingServer server = new PingServer(socket); } }

Jukebox...
class VideoPlaylistIndex { VideoRepository repo; @VisibleForTesting VideoPlaylistIndex( VideoRepository repo) { this.repo = repo; !} VideoPlaylistIndex() { this.repo = new FullLibraryIndex(); } } class PlaylistGenerator { VideoPlaylistIndex index = new VideoPlaylistIndex(); Playlist buildPlaylist(Query q) { return index.search(q); !} }

Jukebox...
class VideoPlaylistIndex { VideoRepository repo; @VisibleForTesting VideoPlaylistIndex( VideoRepository repo) { this.repo = repo; !} VideoPlaylistIndex() { this.repo = new FullLibraryIndex(); } } public void testBadDesignHasNoSeams() { PlaylistGenerator generator = new PlaylistGenerator(); // Doh! Now we're tied to the // VideoPlaylistIndex with the bulky // FullLibraryIndex that will make slow // tests. } class PlaylistGenerator { VideoPlaylistIndex index = new VideoPlaylistIndex(); Playlist buildPlaylist(Query q) { return index.search(q); !} }

Jukebox...

Flaw: PlaylistGenerator is hard to test, because it takes advantage of the no-arg constructor for VideoPlaylistIndex, which is hard coded to using the FullLibraryIndex.You wouldnt really want to test the FullLibraryIndex in a test of the PlaylistGenerator, but you are forced to.

! Flaw: Usually, the @VisibleForTesting annotation is a smell that the class was not written to be easily tested. And even though it will let you set the list of calls, it is only a hack to get around the root problem.

Jukebox...
class VideoPlaylistIndex { VideoRepository repo; VideoPlaylistIndex( VideoRepository repo) { // One constructor to // rule them all this.repo = repo; !} } } class PlaylistGenerator { VideoPlaylistIndex index; // pass in with manual DI PlaylistGenerator( VideoPlaylistIndex index) { this.index = index; } Playlist buildPlaylist(Query q) { return index.search(q); !}

Jukebox...
class VideoPlaylistIndex { VideoRepository repo; VideoPlaylistIndex( VideoRepository repo) { // One constructor to // rule them all this.repo = repo; !} } } class PlaylistGenerator { VideoPlaylistIndex index; // pass in with manual DI PlaylistGenerator( VideoPlaylistIndex index) { this.index = index; } Playlist buildPlaylist(Query q) { return index.search(q); !}

public void testFlexibleDesignWithDI() { VideoPlaylistIndex fakeIndex = new InMemoryVideoPlaylistIndex() PlaylistGenerator generator = new PlaylistGenerator(fakeIndex); }

Death & Taxes


class SalesTaxCalculator { TaxTable taxTable; SalesTaxCalculator(TaxTable taxTable) { this.taxTable = taxTable; } float computeSalesTax(User user, Invoice invoice) { Address address = user.getAddress(); float amount = invoice.getSubTotal(); ! return amount * taxTable.getTaxRate(address); } }

Death & Taxes


class SalesTaxCalculator { TaxTable taxTable; SalesTaxCalculator(TaxTable taxTable) { this.taxTable = taxTable; } float computeSalesTax(User user, Invoice invoice) { Address address = user.getAddress(); float amount = invoice.getSubTotal(); ! return amount * taxTable.getTaxRate(address); } }

SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable()); Address address = new Address("1600 Amphitheatre Parkway..."); User user = new User(address); Invoice invoice = new Invoice(1, new Product(95.00)); assertEquals(0.09, calc.computeSalesTax(user, invoice));

Death & Taxes


! Flaw: To test this class you need to instantiate a User and an Invoice and populate them with a Zip and an amount. This is an extra burden to testing.

! Flaw: For users of this method, it is unclear that all that is needed is an Address and an Invoice. (The API lies to you). ! Flaw: From code reuse point of view, if you wanted to use this class on another project you would also have to supply source code to unrelated classes such as Invoice, and User. (Which in turn may pull in more dependencies)

Death & Taxes


class SalesTaxCalculator { ! TaxTable taxTable; ! SalesTaxCalculator(TaxTable taxTable) { !!! this.taxTable = taxTable; ! } ! float computeSalesTax(Address address, float amount) { !!! return amount * taxTable.getTaxRate(address); ! } }

Death & Taxes


class SalesTaxCalculator { ! TaxTable taxTable; ! SalesTaxCalculator(TaxTable taxTable) { !!! this.taxTable = taxTable; ! } ! float computeSalesTax(Address address, float amount) { !!! return amount * taxTable.getTaxRate(address); ! } }

SalesTaxCalculator calc = new SalesTaxCalculator(new TaxTable()); Address address = new Address("1600 Amphitheatre Parkway..."); assertEquals(0.09, calc.computeSalesTax(address, 95.00));

Making a Mockery...
class LoginPage { RPCClient client; HttpRequest request; LoginPage(RPCClient client, HttpServletRequest request) { this.client = client; this.request = request; } boolean login() { String cookie = request.getCookie(); return client.getAuthenticator() .authenticate(cookie); } }

Making a Mockery...
class LoginPageTest extends TestCase { public void testTooComplicatedThanItNeedsToBe() { Authenticator authenticator = new FakeAuthenticator(); IMocksControl control = EasyMock.createControl(); RPCClient client = control.createMock(RPCClient.class); EasyMock.expect(client.getAuthenticator()) .andReturn(authenticator); HttpServletRequest request = control.createMock(HttpServletRequest.class); Cookie[] cookies = new Cookie[]{new Cookie("g", "xyz123")}; EasyMock.expect(request.getCookies()).andReturn(cookies); control.replay(); LoginPage page = new LoginPage(client, request); // ... assertTrue(page.login()); !! !control.verify(); }

Making a Mockery...
! Flaw: Nobody actually cares about the RPCCllient in this class. Why are we passing it in?

! Flaw: Nobody actually cares about the HttpRequest in this class. Why are we passing it in? ! Flaw: The cookie is what we need, but we must dig into the request to get it. For testing, instantiating an HttpRequest is not a trivial matter. ! Flaw: The Authenticator is the real object of interest, but we have to dig into the RPCClient to get the Authenticator.

Making a Mockery...
class LoginPage { LoginPage(@Cookie String cookie, Authenticator authenticator) { this.cookie = cookie; this.authenticator = authenticator; } boolean login() { return authenticator.authenticate(cookie); } }

Making a Mockery...
class LoginPage { LoginPage(@Cookie String cookie, Authenticator authenticator) { this.cookie = cookie; this.authenticator = authenticator; } boolean login() { return authenticator.authenticate(cookie); } }

public void testMuchEasier() { Cookie cookie = new Cookie("g", "xyz123"); Authenticator authenticator = new FakeAuthenticator(); LoginPage page = new LoginPage(cookie, authenticator); // ... assertTrue(page.login()); }

Making a bigger Mockery...


class UpdateBug { Database db; UpdateBug(Database db) { this.db = db; } void execute(Bug bug) { // Digging around violating Law of Demeter db.getLock().acquire(); try { db.save(bug); } finally { db.getLock().release(); } } }

Making a bigger Mockery...


Bug bug = new Bug("description"); // This both violates Law of Demeter and abuses // mocks, where mocks aren't entirely needed. IMocksControl control = EasyMock.createControl(); Database db = control.createMock(Database.class); Lock lock = control.createMock(Lock.class); // Yikes, this mock (db) returns another mock. EasyMock.expect(db.getLock()).andReturn(lock); lock.acquire(); db.save(bug); EasyMock.expect(db.getLock()).andReturn(lock); !! !lock.release(); control.replay(); // Now we're done setting up mocks, finally! UpdateBug updateBug = new UpdateBug(db); updateBug.execute(bug); // Verify it happened as expected control.verify(); // Note: another test with multiple execute // attempts would need to assert the specific // locking behavior is as we expect.

Making a bigger Mockery...


! Flaw: db.getLock() is outside the single responsibility of the Database. It also violates the law of demeter by requiring us to call db.getLock().acquire() and db.getLock().release() to use the lock.

! Flaw: When testing the UpdateBug class, you will have to mock out the Databases getLock method. ! Flaw: The Database is acting as a database, as well as a service locator (helping others to find a lock). It has an identity crisis. Combining Law of Demeter violations with acting like a Service Locator is worse than either problem individually. The point of the Database is not to distribute references to other services, but to save entities into a persistent store.

Making a bigger Mockery...


class UpdateBug { Database db; Lock lock; UpdateBug(Database db, Lock lock) { this.db = db; } void execute(Bug bug) { lock.acquire(); try { db.save(bug); } finally { lock.release(); } } }

Making a bigger Mockery...


class UpdateBug { Database db; Lock lock; UpdateBug(Database db, Lock lock) { this.db = db; } void execute(Bug bug) { lock.acquire(); try { db.save(bug); } finally { lock.release(); } } } Bug bug = new Bug("description"); InMemoryDatabase db = new InMemoryDatabase(); Lock lock = new Lock(); UpdateBug updateBug = new UpdateBug(db, lock); assertEquals(bug, db.getLastSaved());

Out of Context
class MembershipPlan { void processOrder(UserContext userContext) { User user = userContext.getUser(); PlanLevel level = userContext.getLevel(); Order order = userContext.getOrder(); // ... process } }

Out of Context
class MembershipPlan { void processOrder(UserContext userContext) { User user = userContext.getUser(); PlanLevel level = userContext.getLevel(); Order order = userContext.getOrder(); // ... process } }

MembershipPlan plan = new MembershipPlan(); UserContext userContext = new UserContext(); userContext.setUser(new User("Kim")); PlanLevel level = new PlanLevel(143, "yearly"); userContext.setLevel(level); Order order = new Order("SuperDeluxe", 100, true); userContext.setOrder(order); plan.processOrder(userContext); // Then make assertions against the user, etc ...

Out of Context
! Flaw: Your API says all you need to test this method is a userContext map. But as a writer of the test, you have no idea what that actually is! Generally this means you write a test passing in null, or an empty map, and watch it fail, then progressively stuff things into the map until it will pass.

! Flaw: Some may claim the API is flexible (in that you can add any parameters without changing the signatures), but really it is brittle because you cannot use refactoring tools; users dont know what parameters are really needed. It is not possible to determine what its collaborators are just by examining the API. This makes it hard for new people on the project to understand the behavior and purpose of the class. We say that API lies about its dependencies.

Out of Context
class MembershipPlan { void processOrder(User user, PlanLevel level, Order order) { // ... process } }

Out of Context
class MembershipPlan { void processOrder(User user, PlanLevel level, Order order) { // ... process } }

MembershipPlan plan = new MembershipPlan(); User user = new User("Kim"); PlanLevel level = new PlanLevel(143, "yearly"); Order order = new Order("SuperDeluxe", 100, true); plan.processOrder(user, level, order); // Then make assertions against the user, etc ...

class LoginService { private static LoginService instance = new RealLoginService(); private LoginService() {}; static LoginService getInstance() { return instance; } @VisibleForTesting static setForTest(LoginService testDouble) { instance = testDouble; } @VisibleForTesting static resetForTest() { instance = new RealLoginService(); } }

Singletons are Global State

class AdminDashboard { boolean isAuthenticatedAdminUser(User user) { LoginService loginService = LoginService.getInstance(); return loginService.isAuthenticatedAdmin(user); } }

Singletons are Global State


public void testForcedToUseRealLoginService() { AdminDashborad adminDashboard = new AdminDashboard(); assertTrue(adminDashboard.isAuthenticatedAdminUser(user)); // Arghh! Because of the Singleton, this is // forced to use the RealLoginService() }

public void testMutateGlobalStateForMockLoginService() { AdminDashborad adminDashboard = new AdminDashboard(); // Modifying global state! Forget about parallel execution. LoginService.setForTest(new AlwaysLogidinLoginService()); assertTrue(adminDashboard.isAuthenticatedAdminUser(user)); // Forgetting to call this usually breaks later tests. LoginService.resetForTest(); }

Out of Context
! Flaw: As in all uses of static methods, there are no seams to polymorphically change the implementation. Your code becomes more fragile and brittle.

! Flaw: Tests cannot run in parallel, as each threads mutations to shared global state will collide. ! Flaw: @VisibleForTesting is a hint that the class should be reworked so that it does not need to break encapsulation in order to be tested. Notice how that is removed in the solution.

class LoginService { // removed the static instance // removed the private constructor // removed the static getInstance() // ... keep the rest } bind(LoginService.class) .to(RealLoginService.class) .in(Scopes.SINGLETON);

Out of Context

class AdminDashboard { LoginService loginService; @Inject AdminDashboard(LoginService loginService) { this.loginService = loginService; } boolean isAuthenticatedAdminUser(User user) { return loginService.isAuthenticatedAdmin(user); } }

Singletons are Global State

public void testUsingMockLoginService() { // Testing is now easy, we just pass in a test// double LoginService in the constructor. AdminDashboard dashboard = new AdminDashboard(new MockLoginService()); // ... now all tests will be small and fast }

class RpcClient {

Statics are untestable


static Backend backend; // static // flag static { backend ? new : new } init block gets run ONCE, and whatever is read will be stuck forever. = FLAG_useRealBackend.get() RealBackend() DummyBackend();

static RpcClient client = new RpcClient(); public RpcClient getInstance() { return client; } } class RpcCache { RpcCache(RpcClient client) { } }

Singletons are Global State


// Two tests which fail when run together. public void testXyzWithRealBackend() { FLAG_useRealBackend.set(true); RpcClient client = RpcClient.getInstance(); // ... make assertions that need a real backend // and then clean up ! FLAG_useRealBackend.resetForTest(); } public void testCacheWithDummyBackend() { FLAG_useRealBackend.set(false); RpcCache cache = new RpcCache(RpcClient.getInstance()); // ... make assertions // and then clean up ! FLAG_useRealBackend.resetForTest(); }

Out of Context

Flaw: Static Initialization Blocks are run once, and are nonoverridable by tests

! Flaw: The Backend is set once, and never can be altered for future tests. This may cause some tests to fail, depending on the ordering of the tests.

Out of Context
class RpcClient { Backend backend; @Inject RpcClient(Backend backend) { this.backend = backend; } } class RpcCache { @Inject RpcCache(RpcClient client) { // ... } }

Singletons are Global State


// These tests pass in any order, even if run in parallel. public void testXyzWithRealBackend() { RpcClient client = new RpcClient(new RealBackend()); // ... make assertions that need a real backend } public void testCacheWithDummyBackend() { RpcCache cache = new RpcCache( new RpcClient(new DummyBackend())); // ... make assertions }

// Hard to test, since findNextTrain() will always // call the third party library's static method. class TrainSchedules { Schedule findNextTrain() { // ... do some work and get a track if (TrackStatusChecker.isClosed(track)) { // ... } // ... return a Schedule } }

Train Spotting

// Hard to test, since findNextTrain() will always // call the third party library's static method. class TrainSchedules { Schedule findNextTrain() { // ... do some work and get a track if (TrackStatusChecker.isClosed(track)) { // ... } // ... return a Schedule } } // Testing something like this is difficult, // because the design is flawed. public void testFindNextTrainNoClosings() { // ... assertNotNull(schedules.findNextTrain()); // Phooey! This forces the slow // TrackStatusChecker to get called, // which I don't want in unit tests. }

Train Spotting

Train Spotting
! Flaw: You are forced to execute the TrackStatusCheckers method even when you dont want to, because it is locked in there with a static call.

! Flaw: Tests may be slower, and risk mutating global state through the static in the library. ! Flaw: Static methods are non-overridable and non-injectable. ! Flaw: Static methods remove a seam from your test code.

// Wrap the library in an injectable object of your own. class TrackStatusCheckerWrapper implements StatusChecker { //wrap and delegate to the 3rd party library's methods boolean isClosed(Track track) { return TrackStatusChecker.isClosed(track); } } class TrainSchedules { StatusChecker wrappedLibrary; public TrainSchedules(StatusChecker wrappedLibrary) { this.wrappedLibrary = wrappedLibrary; } Schedule findNextTrain() { // ... // Now delegate to the injected library. if (wrappedLibrary.isClosed(track)) { // ... } // ... return a Schedule } }

Train Spotting

Ideal Interface

Ideal Interface

Complex Interface

Ideal Interface
Complex Third Party API; lots of unneeded methods which return objects which are not quite what we want and need to be marshaled

Complex Interface

Ideal Interface
Complex Third Party API; lots of unneeded methods which return objects which are not quite what we want and need to be marshaled

Complex Interface

Ideal Interface

Ideal Interface
Simplied API for your application which returns the application value objects. This interface becomes great place to insert a fake implementation for scenario testing. Complex Third Party API; lots of unneeded methods which return objects which are not quite what we want and need to be marshaled

Complex Interface

Ideal Interface

Ideal Interface
Simplied API for your application which returns the application value objects. This interface becomes great place to insert a fake implementation for scenario testing. Complex Third Party API; lots of unneeded methods which return objects which are not quite what we want and need to be marshaled

Adapter

Complex Interface

Ideal Interface

Ideal Interface
Simplied API for your application which returns the application value objects. This interface becomes great place to insert a fake implementation for scenario testing. Complex Third Party API; lots of unneeded methods which return objects which are not quite what we want and need to be marshaled

Adapter

Adapter code which bridges the two APIs and marshals the objects to match type impedance. Harder to test, but all of the ugliness is isolated to one location.

Complex Interface

Ideal Interface

Train Spotting
public void testFindNextTrainNoClosings() { StatusCheckerWrapper localWrapper = new StubStatusCheckerWrapper(); TrainSchedules schedules = new TrainSchedules(localWrapper); assertNotNull(schedules.findNextTrain()); // Perfect! This works just as we wanted, // allowing us to test the TrainSchedules in // isolation. }

Q&A

You might also like