How To Write Hard To Test Code
How To Write Hard To Test Code
Agenda
Psychology of Testing Work in Constructor Law of Demeter Inheritance BREAK Global State (Cory Smith) Code Review
Development Model
Software Engineers
QA
Test Engineers
Development Model
Software Engineers
Develop
QA
Test Engineers
Development Model
Software Engineers
Develop
QA
Test Engineers
Development Model
Software Engineers
Develop
QA
Test Engineers
Automated Tests
Development Model
Software Engineers
Develop
QA
Test Engineers
Automated Tests
Tools
Testing magic
Development Model
Software Engineers
Develop
QA
Test Engineers
Automated Tests
Tools
Testing magic
Development Model
Software Engineers
Develop
Design
Testing magic
QA
Test Engineers
Automated Tests
Tools
Testing magic
Test
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
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
Bugs
Bugs
Bugs
Bugs
Bugs
Bugs
MEDIUM
MEDIUM
MEDIUM
MEDIUM
EASY
EASY
I have a Super-Bug!
Unit
Unit
Functional
Unit
Functional
Unit
Functional
Unit
Functional
Unit
Functional
Unit
Functional
Unit
# of Tests
Software Engineers
Functional
Unit
# of Tests
Functional
Unit
# of Tests
Functional
Unit
# of Tests
Functional
Unit
# of Tests
Develop
CI-build Check-In
Test
Review
Develop
CI-build Check-In
Test
Review
Develop
CI-build Check-In
Test
Review
Tools
Develop
CI-build Check-In
Test
Review
Tools
Visibility
Develop
CI-build Check-In
Test
Review
Tools
Visibility
Testing on the Toilet Tech Talks / Blogs 1 on 1 Training Mercenaries Develop Immersion
CI-build Check-In
Test
Review
Tools
Visibility
Testing on the Toilet Tech Talks / Blogs 1 on 1 Training Mercenaries Develop Immersion
CI-build Check-In
Test
Review
Testing on the Toilet Tech Talks / Blogs 1 on 1 Training Mercenaries Develop Immersion
CI-build Check-In
Test
Review
Visibility
Testing on the Toilet Tech Talks / Blogs 1 on 1 Training Mercenaries Develop Immersion
Test
Review
Visibility
Education:
We try to stick it on everyones
monitor
Education: TotT
Weekly publications Testing Tips (not
concepts)
Education: Blog
Venue for larger concepts which do not t
to TotT.
Tools: Coverage
Simple but effective Just having it Squeaky wheel gets
the grease installed makes people thing about improving coverage
Visibility: Games
Visibility: Bubbles
Visibility: Certication
Well dened standards To be at N level you need to
do X,Y, and Z.
TC level prestige
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
Test Driver
Test Driver
Stimulus
Test Driver Class Under Test
Stimulus
Test Driver Class Under Test
Asserts
Test Driver
Test Driver
Test Driver
Test Driver
Other Class
Test Driver
Other Class
Other Class
Test Driver
Other Class
Other Class
Object Lifetime and Calling Object Instantiated Object Passed In Global Object
Other Class
Test Driver
Other Class
Destructive operation
Object Lifetime and Calling Object Instantiated Object Passed In Global Object
Other Class
Other Servers
Test Driver
Other Class
Destructive operation
Object Lifetime and Calling Object Instantiated Object Passed In Global Object
Other Class
Other Servers
Test Driver
Other Class
Destructive operation
Object Lifetime and Calling Object Instantiated Object Passed In Global Object
Other Servers
Seam
Test Driver
Other Class
Destructive operation
Object Lifetime and Calling Object Instantiated Object Passed In Global Object
Other Servers
Business Logic
Business Logic
Seam
Friendly
Friendly
Object Lifetime and Calling Object Instantiated Object Passed In Global Object
Seam
Friendly
Friendly
Object Lifetime and Calling Object Instantiated Object Passed In Global Object
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
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
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
Cost of Construction
class Document { String html; Document does not care about client, it cares about what client can produce
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.
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
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! } }
Composition vs 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
Composition vs Inheritance
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.
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)
Law of Demeter
You only ask for objects which you directly
need (operate on)
Global State
There is no way to
intercept the calls to HardDisk this is to actually format your hard disk and have a post condition
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(); } }
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.
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(); }
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
Service Locator
aka Context Better then a Singleton If you had static look up of services this Hides true dependencies
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
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
Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }
Deceptive API
testCharge() { CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }
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! 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); }
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); }
Deceptive API
testCharge() { Database.connect(...); OfflineQueue.start(); CreditCardProcessor.init(...); CreditCard cc; cc = new CreditCard(1234567890121234); cc.charge(100); }
Better API
testCharge() {
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); }
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. }
! 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.
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. }
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. } }
! 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.
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); }
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));
! 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)
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()); }
! 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.
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(); } }
class AdminDashboard { boolean isAuthenticatedAdminUser(User user) { LoginService loginService = LoginService.getInstance(); return loginService.isAuthenticatedAdmin(user); } }
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); } }
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 {
static RpcClient client = new RpcClient(); public RpcClient getInstance() { return client; } } class RpcCache { RpcCache(RpcClient client) { } }
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) { // ... } }
// 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