Testable JavaScriptArchitecting Your Application forTestabilityMark Ethan TrostlerGoogle Inc.twitter: @zzoassmark@zzo.com
LooseCouplingTightFocusNoSurprisesWhat Is Testability?MinimalTedium
SwapImplementationsWork/Test inParallelInterfaces not ImplementationWrite TestsOnce
Interfaces not Implementationvar UserRepository = {get: function(id) {}, save: function(user) {}, getAll: function() {}, e...
Interfaces not Implementationfunction test(repo) {var id = 99, user = { id: id, ... };repo.save(user);expect(repo.get(id))...
Interfaces not Implementationvar UserRepoRedis = function(host, port, opt) {this.redis = redis.createClient({ ... });};Use...
Interfaces not Implementation• Object is interface - no initialization• Implementation has constructor withinjected depend...
Interfaces not Implementationfunction test(repo) {var user = { ... };repo.save(user);expect(repo.get(id)).toEqual(user);}v...
Interfaces not Implementationvar UserRepoS3 = function(key, secret, bucket) {this.s3 = knox.createClient({...});};UserRepo...
Interfaces not Implementationfunction test(repo) {var id = 99, user = { id: id, ... };repo.save(user);expect(repo.get(id))...
Single Responsibility PrincipleEvery interface should have a singleresponsibility, and that responsibilityshould be entire...
Interface Segregation PrincipleNo object should be forcedto depend on methods itdoes not use.
Interface Pattern• Single Responsibility Principle• Match Sets with Gets• More Smaller / Fewer Bigger• Interface Segregati...
Using InterfacesYouve created nice interfaces - use themwisely!// DO NOT DO THIS!var UserController = function() {this.use...
Using InterfacesYouve created nice interfaces - use themwisely!// DO THIS - Inject the dependencies!var UserController = f...
Liskov Substitution PrincipleAny objects that implementan interface can be usedinterchangeably
Constructor InjectionAll dependencies should be injected into yourobjects constructor*.•  Make dependencies explicit•  Loo...
Instantiating ImplementationsSo do all dependees need to provide fullyinitialized objects to their dependents wheninstanti...
Object Creation vs. Object UsecreationHappens one time at the Composition Root.All Objects* are created/wired together att...
Object CreationWhat creates the objects?Forces specification of dependencies andtheir lifespan•  DIY DI•  wire.js / cujojs...
Cross-Cutting ConcernsWhat about the crap I might need?•  Logging•  Auditing•  Profiling•  Security•  Caching•  ...."Cross...
Cross-Cutting Concerns// DO NOT DO THIS:UserRepoRedis = function(...) {this.redis = ...this.logger = new Logger(); // or L...
Cross-Cutting Concerns// DO NOT DO THIS:UserRepoRedis = function(..., logger, profiler) {this.redis = ...this.logger = log...
Cross-Cutting Concerns// DO NOT DO THIS:UserRepoRedis.prototype.save =function(user) {logger.log(Saving user:  + user);pro...
Cross-Cutting Concerns•  Keep different functionality separate•  Single Responsibility Principle•  Interface Segregation P...
var LoggerFile = function(file) { // Implementationthis.file = file;}LoggerFile.prototype = Object.create(Logger);LoggerFi...
Mixin method - quick aside...// Composition not inheritancevar MIXIN = function(base, extendme) {var prop;for (prop in bas...
Decoratorvar UserRepoLogger = function(repo, logger) {this.innerRepo = repo;this.logger = logger;MIXIN(repo, this); // Mix...
Decorator// UserRepoLogger will Intercept save// all other methods will fall thru to// UserRepoRedis// This userRepo imple...
var ProfilerTime = function() { this.profiles = {}; };ProfilerTime.prototype = Object.create(Profiler);ProfilerTime.protot...
Decoratorvar UserRepoProfiler = function(repo, profiler) {this.innerRepo = repo;this.profiler = profiler;MIXIN(repo, this)...
Interceptvar redisRepo = new UserRepoRedis();var profiler = new ProfilerTime();var logger = new LoggerFile();// Profiling ...
Testing DecoratorsCreate the Decorator and Test the Interface:function testUserRepoLogger(repo) {var id = 99, user = { id:...
Testing Decoratorsvar repo = new UserRepoMock();var logger = new LoggerMock();var profiler = new ProfileMock();var userRep...
Decorator Pattern•  Constructor accepts inner Object of thesame type and any other necessarydependencies.•  Mixin with inn...
var FindMatchesDistance = f(userRepo) { ... };FindMatchesDistance.prototype =Object.create(FindMatches);var FindMatchesAct...
Runtime/Short-lived Dependencies// User Controller needs a findMatches// implementation - but which one???var UserControll...
var FindMatchFactoryImp = function(repo) {this.repo = repo;};FindMatchFactoryImpl.prototype =Object.create(FindMatchFactor...
Runtime/Short-lived DependenciesgetMatchImplementation = function(type) {switch(type) {case likes:return new FindMatchesLi...
Runtime/Short-lived Dependenciesvar UserController = function(findMatchFactory) {this.findMatchFactory = findMatchFactory;...
Testing Abstract Factoriesvar matchTypes = [{ name: likes, type: FindMatchesLikes }, ....];test(findMatchFactory) {matchTy...
Mocking Abstract Factoriesvar TestFactoryImpl = function(expectedType, mockMatch) {this.expectedType = expectedType;this.m...
Abstract Factory Pattern•  Abstract factory translates runtimeparameter -> Dependency•  Factory implementation created atC...
Testable JavaScript•  Composition not Inheritance (impl. lock-in)•  Program and Test to Interfaces / Interfaces arethe API...
Ensuring Testability
Testable JavaScript:  Application Architecture
Upcoming SlideShare
Loading in …5
×

Testable JavaScript: Application Architecture

2,773 views

Published on

Detailed examine of how to architect your JS application for testability

Published in: Technology
0 Comments
2 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
2,773
On SlideShare
0
From Embeds
0
Number of Embeds
17
Actions
Shares
0
Downloads
15
Comments
0
Likes
2
Embeds 0
No embeds

No notes for slide

Testable JavaScript: Application Architecture

  1. 1. Testable JavaScriptArchitecting Your Application forTestabilityMark Ethan TrostlerGoogle Inc.twitter: @zzoassmark@zzo.com
  2. 2. LooseCouplingTightFocusNoSurprisesWhat Is Testability?MinimalTedium
  3. 3. SwapImplementationsWork/Test inParallelInterfaces not ImplementationWrite TestsOnce
  4. 4. Interfaces not Implementationvar UserRepository = {get: function(id) {}, save: function(user) {}, getAll: function() {}, edit: function(id, user) {}, delete: function(id) {}, query: function(query) {}};
  5. 5. Interfaces not Implementationfunction test(repo) {var id = 99, user = { id: id, ... };repo.save(user);expect(repo.get(id)).toEqual(user);}Test the interface
  6. 6. Interfaces not Implementationvar UserRepoRedis = function(host, port, opt) {this.redis = redis.createClient({ ... });};UserRepoRedis.prototype =Object.create(UserRepo);UserRepoRedis.prototype.save =function(user) { ... };
  7. 7. Interfaces not Implementation• Object is interface - no initialization• Implementation has constructor withinjected dependencies• Prototype is Object.create(Interface)• Override with prototype functions
  8. 8. Interfaces not Implementationfunction test(repo) {var user = { ... };repo.save(user);expect(repo.get(id)).toEqual(user);}var repo = new UserRepoRedis(host, port, opt);test(repo);Test the interface
  9. 9. Interfaces not Implementationvar UserRepoS3 = function(key, secret, bucket) {this.s3 = knox.createClient({...});};UserRepoS3.prototype = Object.create(UserRepo);UserRepoS3.prototype.save =function(user) { ... }
  10. 10. Interfaces not Implementationfunction test(repo) {var id = 99, user = { id: id, ... };repo.save(user);expect(repo.get(id)).toEqual(user);}var repo = new UserRepoS3(key, secret, bucket);test(repo);Test the interface
  11. 11. Single Responsibility PrincipleEvery interface should have a singleresponsibility, and that responsibilityshould be entirely encapsulated bythe interface.Responsibility = Reason To Change
  12. 12. Interface Segregation PrincipleNo object should be forcedto depend on methods itdoes not use.
  13. 13. Interface Pattern• Single Responsibility Principle• Match Sets with Gets• More Smaller / Fewer Bigger• Interface Segregation Principle• Test and Program to Interface Only
  14. 14. Using InterfacesYouve created nice interfaces - use themwisely!// DO NOT DO THIS!var UserController = function() {this.userRepo = new UserRepoRedis();};
  15. 15. Using InterfacesYouve created nice interfaces - use themwisely!// DO THIS - Inject the dependencies!var UserController = function(userRepo) {this.userRepo = userRepo;};
  16. 16. Liskov Substitution PrincipleAny objects that implementan interface can be usedinterchangeably
  17. 17. Constructor InjectionAll dependencies should be injected into yourobjects constructor*.•  Make dependencies explicit•  Loose coupling•  Cannot instantiate a non-usable Object* Except:•  runtime dependencies•  objects with a shorter lifetime
  18. 18. Instantiating ImplementationsSo do all dependees need to provide fullyinitialized objects to their dependents wheninstantiating them?NO - THEY DO NOT INSTANTIATE THEM!Object Creation vs. Object Use•  ensures a loosely coupled system•  ensures testability
  19. 19. Object Creation vs. Object UsecreationHappens one time at the Composition Root.All Objects* are created/wired together atthis time•  startup•  RequestuseHappens all the time throughout the application
  20. 20. Object CreationWhat creates the objects?Forces specification of dependencies andtheir lifespan•  DIY DI•  wire.js / cujojs•  Inverted•  Intravenous•  AngularJSwhole lotta others...
  21. 21. Cross-Cutting ConcernsWhat about the crap I might need?•  Logging•  Auditing•  Profiling•  Security•  Caching•  ...."Cross-cutting concerns"
  22. 22. Cross-Cutting Concerns// DO NOT DO THIS:UserRepoRedis = function(...) {this.redis = ...this.logger = new Logger(); // or Logger.get()this.profiler = new Profiler(); // or Profiler.get()};•  tightly coupled to Logger & Profilerimplementations•  PITA to test
  23. 23. Cross-Cutting Concerns// DO NOT DO THIS:UserRepoRedis = function(..., logger, profiler) {this.redis = ...this.logger = logger;this.profiler = profiler;};Injection - so looks good BUT violating SRP!
  24. 24. Cross-Cutting Concerns// DO NOT DO THIS:UserRepoRedis.prototype.save =function(user) {logger.log(Saving user: + user);profiler.startProfiling(saveUser);... do redis/actual save user stuff ...profiler.stopProfiling(saveUser);logger.log(that took: + (start - end));};
  25. 25. Cross-Cutting Concerns•  Keep different functionality separate•  Single Responsibility Principle•  Interface Segregation Principle
  26. 26. var LoggerFile = function(file) { // Implementationthis.file = file;}LoggerFile.prototype = Object.create(Logger);LoggerFile.prototype.log = function(msg) {this.file.write(msg);};Logging Interface and Implementationvar Logger = { // Interfacelog: function(msg) {}, getLastLog: function() {} // get it out!};
  27. 27. Mixin method - quick aside...// Composition not inheritancevar MIXIN = function(base, extendme) {var prop;for (prop in base) {if (typeof base[prop] === function&& !extendme[prop]) {extendme[prop] = base[prop].bind(base);}}};
  28. 28. Decoratorvar UserRepoLogger = function(repo, logger) {this.innerRepo = repo;this.logger = logger;MIXIN(repo, this); // Mixin repos methods};UserRepoLogger.prototype.save =function(user) {this.logger.log(Saving user: + user);return this.innerRepo.save(user);};
  29. 29. Decorator// UserRepoLogger will Intercept save// all other methods will fall thru to// UserRepoRedis// This userRepo implements the UserRepo// interface therefore can be used anywherevar userRepo =new UserRepoLogger(new UserRepoRedis(), new LoggerFile());
  30. 30. var ProfilerTime = function() { this.profiles = {}; };ProfilerTime.prototype = Object.create(Profiler);ProfilerTime.prototype.start = function(id) {this.profiles[id] = new Date().getTimestamp();};ProfilerTime.prototype.stop = function(id) { ... };....Profile Interface and Implementationvar Profiler = { // Interfacestart: function(id) {}, stop: function(id) {}, getProfile: function(id) {} // get it out!};
  31. 31. Decoratorvar UserRepoProfiler = function(repo, profiler) {this.innerRepo = repo;this.profiler = profiler;MIXIN(repo, this); // Mixin repos methods};UserRepoProfiler.prototype.save = f(user) {this.profiler.start(save user);this.innerRepo.save(user);this.profiler.stop(save user);
  32. 32. Interceptvar redisRepo = new UserRepoRedis();var profiler = new ProfilerTime();var logger = new LoggerFile();// Profiling UserRepovar userRepoProf =new UserRepoProfiler(redisRepo, profiler);// Logging Profiling UserRepovar userRepo =new UserRepoLogger(userRepoProf, logger);
  33. 33. Testing DecoratorsCreate the Decorator and Test the Interface:function testUserRepoLogger(repo) {var id = 99, user = { id: id, ... },loggerMock = new LoggerMock(),testRepo =new UserRepoLogger(repo, loggerMock);testRepo.save(user);// verify loggerMock}
  34. 34. Testing Decoratorsvar repo = new UserRepoMock();var logger = new LoggerMock();var profiler = new ProfileMock();var userRepo = new UserRepoProfile(new UserRepoLogger(repo, logger), profiler);// test UserRepo interfacetestRepo(userRepo);// verify logger and profiler mocks
  35. 35. Decorator Pattern•  Constructor accepts inner Object of thesame type and any other necessarydependencies.•  Mixin with inner Object to get defaultbehavior for non-decorated methods.•  Decorate necessary interface methods and(optionally) delegate to inner Object.
  36. 36. var FindMatchesDistance = f(userRepo) { ... };FindMatchesDistance.prototype =Object.create(FindMatches);var FindMatchesActivites = f(userRepo) { ... };var FindMatchesLikes = f(userRepo) { ... };Runtime/Short-lived Dependenciesvar FindMatches = {matches: function(userid) {}};
  37. 37. Runtime/Short-lived Dependencies// User Controller needs a findMatches// implementation - but which one???var UserController = function(findMatches, ...) {....}Inject all three?? What if I make more? What ifother classes need a dynamic findMatchimplementation?
  38. 38. var FindMatchFactoryImp = function(repo) {this.repo = repo;};FindMatchFactoryImpl.prototype =Object.create(FindMatchFactory);Runtime/Short-lived Dependenciesvar FindMatchFactory = {getMatchImplementation: function(type) {}};
  39. 39. Runtime/Short-lived DependenciesgetMatchImplementation = function(type) {switch(type) {case likes:return new FindMatchesLikes(this.repo);break;....default:return new FindMatchesActivites(this.repo);}};
  40. 40. Runtime/Short-lived Dependenciesvar UserController = function(findMatchFactory) {this.findMatchFactory = findMatchFactory;};UserController.prototype = Object.create(Controller);UserController.prototype.findMatches = function(type, uid) {var matcher =this.findMatchFactory.getMatchImplementation(type);return matcher.matches(uid);};
  41. 41. Testing Abstract Factoriesvar matchTypes = [{ name: likes, type: FindMatchesLikes }, ....];test(findMatchFactory) {matchTypes.forEach(function(type) {expect(findMatchFactory.getMatchImplementation(type.name).toEqual(type.type);});}
  42. 42. Mocking Abstract Factoriesvar TestFactoryImpl = function(expectedType, mockMatch) {this.expectedType = expectedType;this.mockMach = mockMatch;};TestFactoryImpl.prototype = Object.create(FindMatchFactory);TestFactoryImpl.prototype.getMatchImplementation(type) {expect(type).toEqual(this.expectedType);return this.mockMatch;};
  43. 43. Abstract Factory Pattern•  Abstract factory translates runtimeparameter -> Dependency•  Factory implementation created atComposition Root along with everythingelse•  Inject factory implementation into theConstructor for runtime dependencies•  Object uses injected factory to getDependency providing Runtime Value
  44. 44. Testable JavaScript•  Composition not Inheritance (impl. lock-in)•  Program and Test to Interfaces / Interfaces arethe API•  Create lots of small Single ResponsibilityInterfaces•  Decorate and Intercept Cross-Cutting Concerns•  Constructor Inject all Dependencies•  Inject Abstract Factory for run-timeDependencies
  45. 45. Ensuring Testability

×