You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// Basic new operators called directly in// the class' constructor. (Forever// preventing a seam to create different// kitchen and bedroom collaborators).classHouse {
Kitchenkitchen = newKitchen();
Bedroombedroom;
House() {
bedroom = newBedroom();
}
// ...
}
- Test
// An attempted test that becomes pretty hardclassHouseTestextendsTestCase {
publicvoidtestThisIsReallyHard() {
Househouse = newHouse();
// Darn! I'm stuck with those Kitchen and// Bedroom objects created in the// constructor. // ...
}
}
After: Testable and Flexible Design
SUT
classHouse {
Kitchenkitchen;
Bedroombedroom;
// Have Guice create the objects// and pass them in@InjectHouse(Kitchenk, Bedroomb) {
kitchen = k;
bedroom = b;
}
// ...
}
- Test
// New and Improved is trivially testable, with any// test-double objects as collaborators.classHouseTestextendsTestCase {
publicvoidtestThisIsEasyAndFlexible() {
KitchendummyKitchen = newDummyKitchen();
BedroomdummyBedroom = newDummyBedroom();
Househouse = newHouse(dummyKitchen, dummyBedroom);
// Awesome, I can use test doubles that// are lighter weight.// ...
}
}
Kitchen์ด value object(LinkdList, Map, User, Email Address ๋ฑ๊ณผ ๊ฐ์ด)๋ผ๋ฉด value object๊ฐ service ๊ฐ์ฒด๋ฅผ ์ฐธ์กฐํ์ง ์๊ธฐ ๋๋ฌธ์ inline์ผ๋ก ์์ฑํ ์ ์๋ค. Service ๊ฐ์ฒด๋ ํ ์คํธ ๋๋ธ๋ก ์นํ๋ ํ์๊ฐ ์๋ ํ์ ์ด๋ค. ๊ทธ๋์ static method ํธ์ถ๋ก ์ง์ ์์ฑํด์๋ ์๋๋ค.
Constructor takes a partially initialized object and has to set it up
Before: Hard to Test
SUT
// SUT initializes collaborators. This prevents// tests and users of Garden from altering them.classGarden {
Garden(Gardenerjoe) {
joe.setWorkday(newTwelveHourWorkday());
joe.setBoots(newBootsWithMassiveStaticInitBlock());
this.joe = joe;
}
// ...
}
- Test
// A test that is very slow, and forced// to run the static init block multiple times.classGardenTestextendsTestCase {
publicvoidtestMustUseFullFledgedGardener() {
Gardenergardener = newGardener();
Gardengarden = newGarden(gardener);
newAphidPlague(garden).infect();
garden.notifyGardenerSickShrubbery();
assertTrue(gardener.isWorking());
}
}
After: Testable and Flexible Design
SUT
// Let Guice create the gardener, and have a// provider configure it.classGarden {
Gardenerjoe;
@InjectGarden(Gardenerjoe) {
this.joe = joe;
}
// ...
}
// In the Module configuring Guice.@ProvidesGardenergetGardenerJoe(Workdayworkday, BootsWithMassiveStaticInitBlockbadBoots) {
Gardenerjoe = newGardener();
joe.setWorkday(workday);
// Ideally, you'll refactor the static init.joe.setBoots(badBoots);
returnjoe;
}
- Test
// The new tests run quickly and are not// dependent on the slow// BootsWithMassiveStaticInitBlockclassGardenTestextendsTestCase {
publicvoidtestUsesGardenerWithDummies() {
Gardenergardener = newGardener();
gardener.setWorkday(newOneMinuteWorkday());
// Okay to pass in null, b/c not relevant// in this test.gardener.setBoots(null);
Gardengarden = newGarden(gardener);
newAphidPlague(garden).infect();
garden.notifyGardenerSickShrubbery();
assertTrue(gardener.isWorking());
}
}
// Violates the Law of Demeter// Brittle because of excessive dependencies// Mixes object lookup with assignmentclassAccountView {
Useruser;
AccountView() {
user = RPCClient.getInstance().getUser();
}
}
- Test
// Hard to test because needs real RPCClientclassACcountViewTestextendsTestCase {
publicvoidtestUnfortunatelyWithRealRPC() {
AccountViewview = newAccountView();
// Shucks! We just had to connect to a real// RPCClient. This test is now slow.// ...
}
}
After: Testable and Flexible Design
SUT
classAccountView {
Useruser;
@InjectAccountView(Useruser) {
this.user = user;
}
}
// The User is provided by a GUICE provider@ProvidesUsergetUser(RPCClientrpcClient) {
returnrpcClient.getUser();
}
// RPCClient is also provided, and it is no longer// a JVM Singleton.@Provides@SingletonRPCClientgetRPCClient() {
// we removed the JVM Singleton// and have GUICE manage the scopereturnnewRPCClient();
}
- Test
// Easy to test with Dependency InjectionclassAccountViewTestextendsTestCase {
publicvoidtestLightweightAndFlexible() {
Useruser = newDummyUser();
AccountViewview = newAccountView(user);
// Easy to test and fast with test-double// user.// ...
}
}
๊ฐ์ ๋ ์ฝ๋์์๋ ์ง์ ์ ์ผ๋ก ํ์ํ ๊ฐ์ฒด๋ง ์ ๋ฌ๋์๋ค: User collaborator. ํ ์คํธ์ ์์ฑํด์ผ ํ๋ ๊ฒ์ (real or test double) User ๊ฐ์ฒด๋ฟ์ด๋ค. ์ด๋ก ์ธํด ์ค๊ณ๊ฐ ๋ณด๋ค ์ ์ฐํด์ง๊ณ ํ ์คํธ ๊ฐ๋ฅ์ฑ์ด ๋ณด๋ค ๋์์ง๋ค.
Creating Unneeded Third Party Objects in Constructor
Before: Hard to Test
SUT
// Creating unneeded third party objects,// Mixing object construction with logic, &// "new" keyword removes a seam for other// EngineFactory's to be used in tests.// Also ties you to the (slow) file system.classCar {
Engineengine;
Car(Filefile) {
Stringmodel = readEngineModel(file);
engine = newEngineFactory().create(model);
}
// ...
}
- Test
// The test exposes the brittleness of the CarclassAccountViewTestextendsTestCase {
publicvoidtestNoSeamForFakeEngine() {
// Aggh! I hate using files in unit testsFilefile = newFile("engine.config");
Carcar = newCar(file);
// I want to test with a fake engine// but I can't since the EngineFactory// only knows how to make real engines.
}
}
After: Testable and Flexible Design
SUT
// Asks for precisely what it needsclassCar {
Engineengine;
@InjectCar(Engineengine) {
this.engine = engine;
}
// ...
}
// Have a provider in the Module// to give you the Engine@ProvidesEnginegetEngine(EngineFactoryengineFactory, @EngineModelStringmodel) {
//returnengineFactory.create(model);
}
// Elsewhere there is a provider to// get the factory and model
- Test
// Now we can see a flexible, injectible designclassAccountViewTestextendsTestCase {
publicvoidtestShowsWeHaveCleanDesign() {
EnginefakeEngine = newFakeEngine();
Carcar = newCar(fakeEngine);
// Now testing is easy, with the car taking// exactly what it needs.
}
}
// Reading flag values to create collaboratorsclassPingServer {
Socketsocket;
PingServer() {
socket = newSocket(FLAG_PORT.get());
}
// ...
}
- Test
// The test is brittle and tied directly to a// Flag's static method (global state).classPingServerTestextendsTestCase {
publicvoidtestWithDefaultPort() {
PingServerserver = newPingServer();
// This looks innocent enough, but really// it forces you to mutate global state// (the flag) to run on another port.
}
}
After: Testable and Flexible Design
SUT
// Best solution (although you also could pass// in an int of the Socket's port to use)classPingServer {
Socketsocket;
@InjectPingServer(Socketsocket) {
this.socket = socket;
}
}
// This uses the FlagBinder to bind Flags to// the @Named annotation values. Somewhere in// a Module's configure method:newFlagBinder(
binder().bind(FlagsClassX.class));
// And the method provider for the Socket@ProvidesSocketgetSocket(@Named("port") intport) {
// The responsibility of this provider is// to give a fully configured Socket// which may involve more than just "new"returnnewSocket(port);
}
- Test
// The revised code is flexible, and easily// tested (without any global state). classPingServerTestextendsTestCase {
publicvoidtestWithNewPort() {
intcustomPort = 1234;
Socketsocket = newSocket(customPort);
PingServerserver = newPingServer(socket);
// ...
}
}
Directly Reading Flags and Creating Objects in Constructor
Before: Hard to Test
SUT
// Branching on flag values to determine state.classCurlingTeamMember {
Jerseyjersey;
CurlingTeamMember() {
if (FLAG_isSuedeJersey.get()) {
jersey = newSuedeJersey();
} else {
jersey = newNylonJersey();
}
}
}
- Test
// Testing the CurlingTeamMember is difficult.// In fact you can't use any Jersey other// than the SuedeJersey or NylonJersey.classCurlingTeamMemberTestextendsTestCase {
publicvoidtestImpossibleToChangeJersey() {
// You are forced to use global state.// ... Set the flag how you want itCurlingTeamMemberruss =
newCurlingTeamMember();
// Tests are locked in to using one// of the two jerseys above.
}
}
After: Testable and Flexible Design
SUT
// We moved the responsibility of the selection// of Jerseys into a provider.classCurlingTeamMember {
Jerseyjersey;
// Recommended, because responsibilities of// Construction/Initialization and whatever// this object does outside it's constructor// have been separated.@InjectCurlingTeamMember(Jerseyjersey) {
this.jersey = jersey;
}
}
// Then use the FlagBinder to bind flags to// injectable values. (Inside your Module's// configure method)newFlagBinder(
binder().bind(FlagsClassX.class));
// By asking for Provider<SuedeJersey>// instead of calling new SuedeJersey// you leave the SuedeJersey to be free// to ask for its dependencies.@ProvidesJerseygetJersey(
Provider<SuedeJersey> suedeJerseyProvider,
Provider<NylonJersey> nylonJerseyProvider,
@Named('isSuedeJersey') suede) {
if (sued) {
returnsuedeJerseyProvider.get();
} else {
returnnylonJerseyProvider.get();
}
}
- Test
// We moved the responsibility of the selection// of Jerseys into a provider.classCurlingTeamMember {
Jerseyjersey;
// Recommended, because responsibilities of// Construction/Initialization and whatever// this object does outside it's constructor// have been separated.@InjectCurlingTeamMember(Jerseyjersey) {
this.jersey = jersey;
}
}
// Then use the FlagBinder to bind flags to// injectable values. (Inside your Module's// configure method)newFlagBinder(
binder().bind(FlagsClassX.class));
// By asking for Provider<SuedeJersey>// instead of calling new SuedeJersey// you leave the SuedeJersey to be free// to ask for its dependencies.@ProvidesJerseygetJersey(
Provider<SuedeJersey> suedeJerseyProvider,
Provider<NylonJersey> nylonJerseyProvider,
@Named('isSuedeJersey') suede) {
if (sued) {
returnsuedeJerseyProvider.get();
} else {
returnnylonJerseyProvider.get();
}
}
Moving the Constructor's "work" into an Initialize Method
Before: Hard to Test
SUT
// With statics, singletons, and a tricky// initialize method this class is brittle.classVisualVoicemail {
Useruser;
List<Call> calls;
@InjectVisualVoicemail(Useruser) {
// Look at me, aren't you proud? I've got// an easy constructor, and I use Guicethis.user = user;
}
initialize() {
Server.readConfigFromFile();
Serverserver = Server.getSingleton();
calls = server.getCallsFor(user);
}
// This was tricky, but I think I figured// out how to make this testable!@VisibleForTestingvoidsetCalls(List<Call> calls) {
this.calls = calls;
}
// ...
}
- Test
// Brittle code exposed through the testclassVisualVoicemailTestextendsTestCase {
publicvoidtestExposesBrittleDesign() {
UserdummyUser = newDummyUser();
VisualVoicemailvoicemail =
newVisualVoicemail(dummyUser);
voicemail.setCalls(buildListOfTestCalls());
// Technically this can be tested, as long// as you don't need the Server to have// read the config file. But testing// without testing the initialize()// excludes important behavior.// Also, the code is brittle and hard to// later on add new functionalities.
}
}
After: Testable and Flexible Design
SUT
// Using DI and Guice, this is a// superior design.classVisualVoicemail {
List<Call> calls;
VisualVoicemail(List<Call> calls) {
this.calls = calls;
}
}
// You'll need a provider to get the calls@ProvidesList<Call> getCalls(Serverserver,
@RequestScopedUseruser) {
returnserver.getCallsFor(user);
}
// And a provider for the Server. Guice will// let you get rid of the JVM Singleton too.@Provides@SingletonServergetServer(ServerConfigconfig) {
returnnewServer(config);
}
@Provides@SingletonServerConfiggetServerConfig(
@Named("serverConfigPath") path) {
returnnewServerConfig(newFile(path));
}
// Somewhere, in your Module's configure()// use the FlagBinder.newFlagBinder(binder().bind(
FlagClassX.class))
- Test
// Dependency Injection exposes your// dependencies and allows for seams to// inject different collaborators.classVisualVoicemailTestextendsTestCase {
VisualVoicemailvoicemail =
newVisualVoicemail(
buildListOfTestCalls());
// ... now you can test this however you want.
}
Having Multiple Constructors, where one is Just for Testing
Before: Hard to Test
SUT
// Half way easy to construct. The other half// expensive to construct. And for collaborators// that use the expensive constructor - they// become expensive as well.classVideoPlaylistIndex {
VideoRepositoryrepo;
@VisibleForTestingVideoPlaylistIndex(
VideoRepositoryrepo) {
// Look at me, aren't you proud?// An easy constructor for testing!this.repo = repo;
}
VideoPlaylistIndex() {
this.repo = newFullLibraryIndex();
}
// ...
}
// And a collaborator, that is expensive to build// because the hard coded index construction.classPlaylistGenerator {
VideoPlaylistIndexindex =
newVideoPlaylistIndex();
PlaylistbuildPlaylist(Queryq) {
returnindex.search(q);
}
}
- Test
// Testing the VideoPlaylistIndex is easy,// but testing the PlaylistGenerator is not!classPlaylistGeneratorTestextendsTestCase {
publicvoidtestBadDesignHasNoSeams() {
PlaylistGeneratorgenerator =
newPlaylistGenerator();
// Doh! Now we're tied to the// VideoPlaylistIndex with the bulky// FullLibraryIndex that will make slow// tests.
}
}
After: Testable and Flexible Design
SUT
// Easy to construct, and no other objects are// harmed by using an expensive constructor.classVideoPlaylistIndex {
VideoRepositoryrepo;
VideoPlaylistIndex(
VideoRepositoryrepo) {
// One constructor to rule them allthis.repo = repo;
}
}
// And a collaborator, that is now easy to// build.classPlaylistGenerator {
VideoPlaylistIndexindex;
// pass in with manual DIPlaylistGenerator(
VideoPlaylistIndexindex) {
this.index = index;
}
PlaylistbuildPlaylist(Queryq) {
returnindex.search(q);
}
}
- Test
// Easy to test when Dependency Injection// is used everywhere. classPlaylistGeneratorTestextendsTestCase {
publicvoidtestFlexibleDesignWithDI() {
VideoPlaylistIndexfakeIndex =
newInMemoryVideoPlaylistIndex()
PlaylistGeneratorgenerator =
newPlaylistGenerator(fakeIndex);
// Success! The generator does not care// about the index used during testing// so a fakeIndex is passed in.
}
}
๊ฐ์ฒด๋ฅผ ์ฐพ๊ฑฐ๋ ์ค์ ํ๋ ์ฑ ์์ factory๋ DI Container(spring)์ ์์ํ๋ผ.
Examples by Diagrams
์๋ชป๋ ์
์๋ ์
Examples by Codes
Service Object Digging Around in Value Object
Before: Hard to Test
SUT
// This is a service object that works with a value// object (the User and amount). classSalesTaxCalculator {
TaxTabletaxTable;
SalesTaxCalculator(TaxTabletaxTable) {
this.taxTable = taxTable;
}
floatcomputeSalesTax(Useruser, Invoiceinvoice) {
// note that "user" is never used directlyAddressaddress = user.getAddress(); // holderfloatamount = invoice.getSubTotal(); // holderreturnamount * taxTable.getTaxRate(address);
}
}
- Test
// Testing exposes the problem by the amount of work// necessary to build the object graph, and test the// small behavior you are interested in.classSalesTaxCalculatorTestextendsTestCase {
SalesTaxCalculatorcalc = newSalesTaxCalculator(newTaxTable());
// So much work wiring together all the objects neededAddressaddress = newAddress("1600 Amphitheatre Parkway...");
Useruser = newUser(address);
Invoiceinvoice = newInvoice(1, newProductX(95.00));
// ...assertEquals(0.09, calc.computeSalesTax(user, invoice), 0.05);
}
After: Testable and Flexible Design
SUT
// Reworked, it only asks for the specific objects// that it needs to collaborate with.classSalesTaxCalculator {
TaxTabletaxTable;
SalesTaxCalculator(TaxTabletaxTable) {
this.taxTable = taxTable;
}
// Note that we no longer use User, nor do we dig inside// the address. (Note: We would use a Money, BigDecimal,// etc. in reality).floatcomputeSalesTax(Addressaddress, floatamount) {
returnamount * taxTable.getTaxRate(address);
}
}
- Test
// The new API is clearer in what collaborators it needs.classSalesTaxCalculatorTestextendsTestCase {
SalesTaxCalculatorcalc = newSalesTaxCalculator(newTaxTable());
// Only wire together the objects that are neededAddressaddress = newAddress("1600 Amphitheatre Parkway...");
// ...assertEquals(0.09, calc.computeSalesTax(address, 95.00), 0.05);
}
}
Service Object Directly Violating "Law of Demeter"
Before: Hard to Test
SUT
// This is a service object which violates the// Law of Demeter. classLoginPage {
RPCClientclient;
HttpRequestrequest;
LoginPage(RPCClientclient, HttpServletRequestrequest) {
this.client = client;
this.request = request;
}
booleanlogin() {
Stringcookie = request.getCookie();
returnclient.getAuthenticator().authenticate(cookie);
}
}
- Test
// The extensive and complicated easy mock usage is// a clue that the design is brittle.classLoginPageTestextendsTestCase {
publicvoidtestTooComplicatedThanItNeedsToBe() {
Authenticatorauthenticator = newFakeAuthenticator();
IMocksControlcontrol = EasyMock.createControl();
RPCClientclient = control.createMock(RPCClient.class);
EasyMock.expect(client.getAuthenticator()).andReturn(authenticator);
HttpServletRequestrequest = control.createMock(HttpServletRequest.class);
Cookie[] cookies = newCookie[]{newCookie("g", "xyz123")};
EasyMock.expect(request.getCookies()).andReturn(cookies);
control.replay();
LoginPagepage = newLoginPage(client, request);
// ...assertTrue(page.login());
control.verify();
}
Ater: Testable and Flexible Design
SUT
// The specific object we need is passed in// directly.classLoginPage {
LoginPage(@CookieStringcookie, Authenticatorauthenticator) {
this.cookie = cookie;
this.authenticator = authenticator;
}
booleanlogin() {
returnauthenticator.authenticate(cookie);
}
}
- Test
// Things now have a looser coupling, and are more// maintainable, flexible, and testable.classLoginPageTestextendsTestCase {
publicvoidtestMuchEasier() {
Cookiecookie = newCookie("g", "xyz123");
Authenticatorauthenticator = newFakeAuthenticator();
LoginPagepage = newLoginPage(cookie, authenticator);
// ...assertTrue(page.login());
}
}
Law of Demeter Violated to Inappropriately make a Service Locator
Before: Hard to Test
SUT
// Database has an single responsibility identity// crisis.classUpdateBug {
Databasedb;
UpdateBug(Databasedb) {
this.db = db;
}
voidexecute(Bugbug) {
// Digging around violating Law of Demeterdb.getLock().acquire();
try {
db.save(bug);
} finally {
db.getLock().release();
}
}
}
- Test
// Testing even the happy path is complicated with all// the mock objects that are needed. Especially// mocks that take mocks (very bad).classUpdateBugTestextendsTestCase {
publicvoidtestThisIsRidiculousHappyPath() {
Bugbug = newBug("description");
// This both violates Law of Demeter and abuses// mocks, where mocks aren't entirely needed.IMocksControlcontrol = EasyMock.createControl();
Databasedb = control.createMock(Database.class);
Locklock = 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!UpdateBugupdateBug = newUpdateBug(db);
updateBug.execute(bug);
// Verify it happened as expectedcontrol.verify();
// Note: another test with multiple execute// attempts would need to assert the specific// locking behavior is as we expect.
}
}
Ater: Testable and Flexible Design
SUT
// The revised Database has a Single Responsibility.classUpdateBug {
Databasedb;
Locklock;
UpdateBug(Databasedb, Locklock) {
this.db = db;
}
voidexecute(Bugbug) {
// the db no longer has a getLock methodlock.acquire();
try {
db.save(bug);
} finally {
lock.release();
}
}
}
// Note: In Database, the getLock() method was removed
- Test
// Two improved solutions: State Based Testing// and Behavior Based (Mockist) Testing.// First Sol'n, as State Based Testing.classUpdateBugStateBasedTestextendsTestCase {
publicvoidtestThisIsMoreElegantStateBased() {
Bugbug = newBug("description");
// Use our in memory version instead of a mockInMemoryDatabasedb = newInMemoryDatabase();
Locklock = newLock();
UpdateBugupdateBug = newUpdateBug(db, lock);
// Utilize State testing on the in memory db.assertEquals(bug, db.getLastSaved());
}
}
// Second Sol'n, as Behavior Based Testing.// (using mocks).classUpdateBugMockistTestextendsTestCase {
publicvoidtestBehaviorBasedTestingMockStyle() {
Bugbug = newBug("description");
IMocksControlcontrol = EasyMock.createControl();
Databasedb = control.createMock(Database.class);
Locklock = control.createMock(Lock.class);
lock.acquire();
db.save(bug);
lock.release();
control.replay();
// Two lines less for setting up mocks.UpdateBugupdateBug = newUpdateBug(db, lock);
updateBug.execute(bug);
// Verify it happened as expectedcontrol.verify();
}
}
You should never have to mock out a setter or getter
Object Called "Context" is a Great Big Hint to look for a Violation
Before: Hard to Test
SUT
// Context objects can be a java.util.Map or some// custom grab bag of stuff.classMembershipPlan {
voidprocessOrder(UserContextuserContext) {
Useruser = userContext.getUser();
PlanLevellevel = userContext.getLevel();
Orderorder = userContext.getOrder();
// ... process
}
}
- Test
// An example test method working against a// wretched context object.publicvoidtestWithContextMakesMeVomit() {
MembershipPlanplan = newMembershipPlan();
UserContextuserContext = newUserContext();
userContext.setUser(newUser("Kim"));
PlanLevellevel = newPlanLevel(143, "yearly");
userContext.setLevel(level);
Orderorder = newOrder("SuperDeluxe", 100, true);
userContext.setOrder(order);
plan.processOrder(userContext);
// Then make assertions against the user, etc ...
}
Ater: Testable and Flexible Design
SUT
// Replace context with the specific parameters that// are needed within the method.classMembershipPlan {
voidprocessOrder(Useruser, PlanLevellevel, Orderorder) {
// ... process
}
}
- Test
// The new design is simpler and will easily evolve.publicvoidtestWithHonestApiDeclaringWhatItNeeds() {
MembershipPlanplan = newMembershipPlan();
Useruser = newUser("Kim");
PlanLevellevel = newPlanLevel(143, "yearly");
Orderorder = newOrder("SuperDeluxe", 100, true);
plan.processOrder(user, level, order);
// Then make assertions against the user, etc ...
}
}
// A DSL may be an acceptable violation.// i.e. in a GUICE Module's configure methodbind(Some.class)
.annotatedWith(Annotation.class)
.to(SomeImplementaion.class)
.in(SomeScope.class);