java - Preferable way of making code testable: Dependency injection vs encapsulation -
i find myself wondering best practice these problems. example:
i have java program should air temperature weather web service. encapsulate in class creates httpclient , rest request weather service. writing unit test class requires stub httpclient dummy data can received in stead. there som options how implement this:
dependency injection in constructor. breaks encapsulation. if switch soap web service in stead, soapconnection has injected instead of httpclient.
creating setter purpose of testing. "normal" httpclient constructed default, possible change httpclient using setter.
reflection. having httpclient private field set constructor (but not taking parameter), , let test use reflection change stubbed one.
package private. lower field restriction make accessible in test.
when trying read best practices on subject seems me general consensus dependency injection preferred way, think downside of breaking encapsulation not given enough thought.
what think preferred way make class testable?
i believe best way through dependency injection, not quite way describe. instead of injecting httpclient
directly, instead inject weatherstatusservice
(or equivalent name). make simple interface 1 method (in use case) getweatherstatus()
. can implement interface httpclientweatherstatusservice
, , inject @ runtime. unit test core class, have choice of stubbing interface implementing weatherstatusservice
own unit testing requirements, or using mocking framework mock getweatherstatus
method. main advantages of way that:
- you don't break encapsulation (because changing soap implementation involves creating
soapweatherstatusservice
, deleting httpclient handler). - you have broken initial single class down, , have 2 classes distinct purpose, 1 class explicitly handles retrieving data api, other class handles core logic. flow like: receive weather status request (from higher up) -> request data retrieval api -> process/validate returned data -> (optionally) store data or trigger other processes operate on data -> return data.
- you can re-use
weatherstatusservice
implementation if different use case emerges utilise data. (for example, perhaps have 1 use case store weather conditions every 4 hours (to show user interactive map of days' developments), , use case current weather. in case, need 2 different core logic requirements both need use same api, makes sense have api access code consistent between these approaches).
this method known hexagonal/onion architecture recommend reading here:
- http://alistair.cockburn.us/hexagonal+architecture
- http://jeffreypalermo.com/blog/the-onion-architecture-part-1/
or post sums core ideas up:
edit:
further comments:
what testing httpclientweatherstatus? ignore unit testing or else have find way mock httpclient there?
with httpclientweatherstatus
class. should ideally immutable, httpclient
dependency injected constructor on creation. makes unit testing easy because can mock httpclient
, prevent interaction outside world. example:
public class httpclientweatherstatusservice implements weatherstatusservice { private final httpclient httpclient; public httpclientweatherstatusservice(httpclient httpclient) { this.httpclient = httpclient; } public weatherstatus getweatherstatus(string location) { //setup request. //make request injected httpclient. //parse response. return new weatherstatus(temperature, humidity, weathertype); } }
where returned weatherstatus
'event' is:
public class weatherstatus { private final float temperature; private final float humidity; private final string weathertype; //constructor , getters. }
then tests this:
public weatherstatusservicetests { @test public void givenalocation_whenaweatherstatusrequestismade_thenthecorrectstatusforthatlocationisreturned() { //setup test. //create httpclient mock. string location = "the world"; //create expected response. //expect request containing location, return response. weatherstatusservice service = new httpclientweatherstatusservice(httpclient); //replay mock. //run test. weatherstatus status = service.getweatherstatus(location); //verify test. //assert status contains correctly parsed response. } }
you find there few conditionals , loops in integration layers (because these constructs represent logic, , logic should in core). because of (specifically because there single conditional branching path in calling code), people argue there little point unit testing class, , can covered integration test easily, , in less brittle way. understand viewpoint, , don't have problem skipping unit tests in integration layers, unit test anyway. because believe unit tests in integration domain still me ensure class highly usable, , portable/re-usable (if it's easy test, it's easy use elsewhere in codebase). use unit tests documentation detailing use of class, advantage ci server alert me when documentation out of date.
isn't bloating code small problem have been "fixed" lines using reflection or changing package private field access?
the fact put "fixed" in quotes speaks volumes how valid think such solution be. ;) agree there bloat code, , can disconcerting @ first. real point make maintainable codebase easy develop for. think projects start fast because "fix" problems using hacks , dodgy coding practices maintain pace. productivity grinds halt overwhelming technical debt renders changes should 1 liners mammoth re-factors take weeks or months.
once have project set in hexagonal way, real payoffs come when need 1 of following:
change technology stack of 1 of integration layers. (e.g. mysql postgres). in case (as touched on above), implement new persistence layer making sure use relevant interfaces binding/event/adapter layer. there should no need change core code or interface. delete old layer, , inject new layer in place.
add new feature. integration layers exist, , may not need modification used. in example of
getcurrentweather()
,store4hourlyweather()
use-cases above. let's assume you've implementedstore4hourlyweather()
functionality using class outlined above. create new functionality (let's assume process begins restful request), need make 3 new files. need new class in web layer handle initial request, need new class in core layer represent user story ofgetcurrentweather()
, , need interface in binding/event/adaptor layer core class implements, , web class has injected constructor. on 1 hand, yes, you've created 3 files when have been possible create 1 file, or tack onto existing restful web handler. of course could, , in simple example work fine. on time distinction between layers become obvious , refactors become hard. consider in case tack onto existing class, class no longer has obvious single purpose. call it? how know in code? how complicated test set-up becoming can test class there more dependencies mock?update integration layer changes. following on example above, if weather service api (where getting information from) changes, there 1 place need make changes in program compatible new api again. place in code knows data comes from, it's place needs changing.
introduce project new team member. arguable point, since laid out project easy understand, experience far has been code looks simple , understandable. achieves 1 thing, , it's @ achieving 1 thing. understanding (for example) amazon-s3 related code obvious because there entire layer devoted interacting it, , layer have no code in relating other integration concerns.
fix bugs. linked above, reproducibility biggest step towards fix. advantage of integration layers being immutable, independent, , accepting clear parameters, is easy isolate single failing layer , modify parameters until fails. (although again, designed code too).
i hope i've answered questions, let me know if have more. :) perhaps creating sample hexagonal project on weekend , linking here demonstrate point more clearly.
Comments
Post a Comment