SlideShare a Scribd company logo
1 of 104
Download to read offline
TRUE 
END-TO-END 
TESTING 
SCALA 
Orr Sella 
IN 
Scalapeño 2014
AGENDA 
WHAT ARE THEY 
WHY WE NEED THEM 
HOW TO WRITE THEM 
END-TO-END 
TESTS 
TEST HARNESS 
DRIVERS 
FAKES 
…
AGENDA 
WHAT ARE THEY 
WHY WE NEED THEM 
HOW TO WRITE THEM 
END-TO-END 
TESTS 
TEST HARNESS 
DRIVERS 
FAKES 
… CO N C R E T E E X A M P L E 
github.com/orrsella/scala-e2e-testing
Disclaimer 
BIG SUBJECT, 
WE CAN’T COVER 
EVERYTHING
Why 
SCALA & END-TO-END 
TESTS 
1 THE APPS 
Typical Scala applications are ripe for end-to-end 
tests 
! 
THE LANGUAGE & ECOSYSTEM 
Build tools, testing libraries and language 
features are a great fit 
2
First, 
SOME TERMINOLOGY
The Bible: GOOS 
INTRO TO TDD 
! 
TESTING 
TECHNIQUES 
! 
LOTS OF CODE & 
EXAMPLES
THELEVELS OF 
TESTING KNOWN TO MAN 
1 UNIT 
Do our objects do the right thing? 
! 
INTEGRATION 
Does our code work against code we can’t change? 
! 
END-TO-END 
Does the whole system work? 
2 
3 
(not really)
WHAT IS 
THE 
RIGHT BALANCE 
OF THE THREE?
THEINVERSE 
ICE CREAMCONE(or pyramid) 
NUMBER OF 
TESTS
THEINVERSE 
ICE CREAMCONE(or pyramid) 
END-TO- 
END 
INTEGRATION 
UNIT 
NUMBER OF 
TESTS
THEINVERSE 
ICE CREAMCONE(or pyramid) 
END-TO- 
END 
INTEGRATION 
UNIT 
MANUAL TESTING 
(if you must) NUMBER OF 
TESTS
WHAT 
ARE 
END-TO-END 
TESTS?
END-TO-END TESTS 
Interact with the system from the outside 
Black Box 
MOST IMPORTANTLY: 
Exercising both the system and the 
process by which it’s built and deployed 
Include interaction with external 
environment
AN IDEALAUTOMATIC 
BUILDWOULD 
1 Compile and unit-test the code 
Integrate and package the system 
Perform a production-like deployment to a 
realistic environment 
Exercise the system through its external access 
points 
2 
3 
4
AN IDEALAUTOMATIC 
BUILDWOULD 
1 Compile and unit-test the code 
Integrate and package the system 
Perform a production-like deployment to a 
realistic environment 
Exercise the system through its external access 
points 
2 
3 
4
Whoa, 
THIS SOUNDS LIKE A 
LOT OF WORK
Whoa, 
THIS SOUNDS LIKE A 
LOT OF WORK 
IT IS.
But, 
IS IT WORTH IT?
But, 
IS IT WORTH IT? 
YES. (I hope to convince you)
WHY 
ARE 
END-TO-END 
TESTS SO 
IMPORTANT?
HERE ARE3 MAIN 
(many more...) 
REASONS
1 
FLUSH OUT 
THE UNKNOWNS 
Requires asking (and answering) many 
awkward questions 
Expose uncertainty early, including 
technical and organizational risks 
Forces to understand how a system fits 
into the world 
Identify all the integration (read: potential 
failure) points
1 
FLUSH OUT 
THE UNKNOWNS 
Requires asking (and answering) many 
awkward questions 
Expose uncertainty early, including 
technical and organizational risks 
Forces to understand how a system fits 
into the world 
Identify all the integration (read: potential 
failure) points 
EXAMPLE IN 
3 SLIDES
2 
FEEDBACK & 
CONFIDENCE 
Feedback that would otherwise only show 
in staging/production 
Safety net of system-wide regression 
Assurance that our chosen technology 
stack (“plumbing”) works as expected 
Confidence to make “risky” system-wide 
changes (e.g: swap datastore, application 
server)
3 
FORCED 
AUTOMATION 
Everything must be automated so it 
can be tested 
Crucial for deployment which is 
error-prone 
Automatic Build-Deploy-Test cycle 
means we can ship frequently
TIME 
FOR A 
CONCRETE 
EXAMPLE:
TIME 
FOR A 
CONCRETE 
EXAMPLE: 
Memento
TIME 
FOR A 
CONCRETE 
EXAMPLE: 
Memento 
github.com/orrsella/scala-e2e-testing
Memento? 
NOTE-TAKING 
WEB SERVICE
Memento? 
NOTE-TAKING 
WEB SERVICE 
STOR E, RE TRIEVE, SEARC H, TRANSLATE
Memento? 
NOTE-TAKING 
WEB SERVICE 
STOR E, RE TRIEVE, SEARC H, TRANSLATE “MICROSERVICE”
Memento? 
NOTE-TAKING 
WEB SERVICE 
STOR E, RE TRIEVE, SEARC H, TRANSLATE “MICROSERVICE”REST API
LOAD 
BALANCER 
HAPROXY 
REVERSE 
PROXY 
NGINX 
APP 
SERVER 
FINATRA 
DATASTORE 
ELASTICSEARCH 
TRANSLATION 
SERVICE 
YANDEX
LOAD 
BALANCER 
HAPROXY 
REVERSE 
PROXY 
NGINX 
APP 
SERVER 
FINATRA 
DATASTORE 
ELASTICSEARCH 
TRANSLATION 
SERVICE 
YANDEX SCALA!..
LOAD 
BALANCER 
HAPROXY 
REVERSE 
PROXY 
NGINX 
APP 
SERVER 
FINATRA 
DATASTORE 
ELASTICSEARCH 
TRANSLATION 
SERVICE 
YANDEX SCALA!.. 
PRETTY 
STRAIGHTFORWARD
Ok. So. 
WHAT CAN POSSIBLY 
GO WRONG & BE 
MISCONFIGURED? 
i.e. integration (failure) points
LOAD 
BALANCER 
HAPROXY 
PORT 80 /ETC/HAPROXY/ 
HAPROXY.CFG 
LIST OF REVERSE 
PROXIES:PORTS 
/ETC/DEFAULT/ 
HAPROXY
REVERSE 
PROXY 
NGINX 
APP 
SERVER 
FINATRA 
/ETC/NGINX/ 
NGINX.CONF 
PROXY 
PORT 7770 
INIT.D 
SCRI P T 
/ETC/NGINX/ 
SITES-AVAILABLE 
APPLICATION.CONF 
LO G BAC K . XM L 
JAVA 
DEPLOY NEW 
VERSION
DATASTORE 
ELASTICSEARCH 
TRANSLATION 
SERVICE 
YANDEX 
PORT 9300 MAPPING 
URL API KEY 
/ETC/ELSATICSEARCH.YML 
JAVA
(just a partial list…)
Ok, 
SO A LOT 
CAN 
(read: will) 
GO WRONG
Ok, 
SO A LOT 
CAN 
(read: will) 
GO WRONG 
WE WANT OUR END-TO-END TESTS 
TO E X E RC I S E A L L O F T H I S !
HOW DO 
WE TEST 
ALL OF THIS 
END-TO-END?
HOW DO 
WE TEST 
ALL OF THIS 
END-TO-END? 
here is one approach…
HOW DO 
WE TEST 
ALL OF THIS 
END-TO-END? 
here is one approach… 
THERE ARE 
OT H E R S
HOW DO 
WE TEST 
ALL OF THIS 
END-TO-END? 
here is one approach… 
THERE ARE 
OT H E R S 
BUT THIS IS THE 
BEST, OBVIOUSLY
STEPSREQUIRED FOR 
END-TO-ENDTESTS 
0 
1 
BUILD & PACKAGE 
Package the app for production deployment 
2 
3 
VIRTUAL MACHINE 
Fire-up a local VM instance with production-like env 
CONFIGURATION & DEPLOYMENT 
Run production config and deployment code, 
having our entire system on one box 
RUN TESTS 
Execute end-to-end tests against the VM
HOW DO 
WE 
ORCHESTRATE 
ALL OF THIS?
Enter: 
THE TEST 
HARNESS 
EXTENSION OF THE BUILD 
Runs all the necessary setup/teardown actions and 
the tests themselves 
! 
(aka sbt) 
STANDARD/SIMPLE/SCALA BUILD TOOL 
Build and package our app (sbt-native-packager) 
Fire-up the virtual machine (Vagrant) 
Run configuration and deployment scripts (Ansible)
Meet 
VAGRANT 
PORTABLE DEV ENVIRONMENTS 
Lightweight, reproducible and programmable 
! 
VIRTUALIZATION 
Wrapper around VirtualBox, VMWare, more 
! 
SIMPLE & QUICK 
Vagrantfile => `$ vagrant up`
THESTEPS REQUIRED FOR 
END-TO-ENDTESTS 
BUILD & PACKAGE 
Package the app for production-like deployment 
0 
1 VIRTUAL MACHINE 
Fire-up a local VM instance with production-like env 
2 CONFIGURATION & DEPLOYMENT 
Run production config and deployment code, 
having our entire system on one box 
3 
RUN TESTS 
Execute end-to-end tests against the VM
Test Harness: 
SBT 
MANAGING 
VAGRANT 
// project/Vagrant.scala 
! 
object Vagrant { 
! 
private lazy val vagrant = settingKey[Vagrant]("vagrant") 
! 
lazy val settings = Seq( 
test in EndToEndTest <<= (test in EndToEndTest).dependsOn(publishLocal), 
testOptions in EndToEndTest += Tests.Setup(() => vagrant.value.setup()), 
testOptions in EndToEndTest += Tests.Cleanup(() => vagrant.value.cleanup()) 
) 
}
Test Harness: 
SBT 
MANAGING 
VAGRANT 
// project/Vagrant.scala 
! 
object Vagrant { 
! 
private lazy val vagrant = settingKey[Vagrant]("vagrant") 
! 
lazy val settings = Seq( 
test in EndToEndTest <<= (test in EndToEndTest).dependsOn(publishLocal), 
testOptions in EndToEndTest += Tests.Setup(() => vagrant.value.setup()), 
testOptions in EndToEndTest += Tests.Cleanup(() => vagrant.value.cleanup()) 
) 
} 
TEARDOWN 
HOOK 
SETUP HOOK
Test Harness: 
SBT 
MANAGING VAGRANT 
class Vagrant(vagrantFile: File) { 
! 
// cli method wrappers 
private def up() = Process("vagrant" :: "up" :: Nil, dir)! 
private def provision() = Process("vagrant" :: "provision" :: Nil, dir)! 
! 
def setup(): Unit = { 
prevStatus = status() 
prevStatus match { 
case Running => provision() 
case Saved => up(); provision() 
case NotCreated => up() 
case Unknown => up() 
} 
} 
! 
def cleanup(): Unit = if (prevStatus != Running) suspend() 
}
Test Harness: 
SBT 
MANAGING VAGRANT 
class Vagrant(vagrantFile: File) { 
! 
// cli method wrappers 
private def up() = Process("vagrant" :: "up" :: Nil, dir)! 
private def provision() = Process("vagrant" :: "provision" :: Nil, dir)! 
! 
def setup(): Unit = { 
prevStatus = status() 
prevStatus match { 
case Running => provision() 
case Saved => up(); provision() 
case NotCreated => up() 
case Unknown => up() 
} 
} 
! 
def cleanup(): Unit = if (prevStatus != Running) suspend() 
}
Test Harness: 
SBT 
MANAGING VAGRANT 
class Vagrant(vagrantFile: File) { 
! 
// cli method wrappers 
private def up() = Process("vagrant" :: "up" :: Nil, dir)! 
private def provision() = Process("vagrant" :: "provision" :: Nil, dir)! 
! 
def setup(): Unit = { 
Tip: VA G R A N T S TAT E 
“J U G G L I N G ” , S AV E S A L O T 
prevStatus = status() 
prevStatus match { 
case Running => provision() 
case Saved => up(); provision() 
case NotCreated => up() 
case Unknown => up() 
} 
} 
! 
def cleanup(): Unit = if (prevStatus != Running) suspend() 
} 
OF TIME!
THESTEPS REQUIRED FOR 
END-TO-ENDTESTS 
0 
1 
BUILD & PACKAGE 
Package the app for production-like deployment 
2 CONFIGURATION & DEPLOYMENT 
3 
VIRTUAL MACHINE 
Fire-up a local VM instance with production-like env 
Run production config and deployment code, 
having our entire system on one box 
RUN TESTS 
Execute end-to-end tests against the VM
THEDEPLOYMENT 
SCRIPTS 
(Provision in Vagrant parlance) 
CONFIGURATION MANAGEMENT 
Ensure that our system is configured properly 
(OS, settings, packages, file system, etc.) 
! 
DEPLOY THE APP 
Upgrade to the latest version just compiled and 
packaged, as would be done in production 
! 
APPLICATION CONFIG 
Configure the application itself the same way it 
would in production (but with test values)
Specifically, 
ANSIBLE 
BUT ANY 
CM TOOL WILL DO 
(Chef, Puppet, Shell scripts)
Provision: 
VAGRANTFILE 
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 
! 
config.vm.box = "ubuntu/trusty64" 
config.vm.network "forwarded_port", guest: 7770, host: 7769 
... 
! 
config.vm.provision "ansible" do |ansible| 
ansible.playbook = "ansible/site.yml" 
ansible.inventory_path = "ansible/inventories/vagrant" 
end 
! 
end
Provision: 
VAGRANTFILE 
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 
! 
config.vm.box = "ubuntu/trusty64" 
config.vm.network "forwarded_port", guest: 7770, host: 7769 
... 
! 
config.vm.provision "ansible" do |ansible| 
ansible.playbook = "ansible/site.yml" 
ansible.inventory_path = "ansible/inventories/vagrant" 
end 
! 
end
Provision: 
VAGRANTFILE 
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 
! 
config.vm.box = "ubuntu/trusty64" 
config.vm.network "forwarded_port", guest: 7770, host: 7769 
... 
! 
config.vm.provision "ansible" do |ansible| 
ansible.playbook = "ansible/site.yml" 
ansible.inventory_path = "ansible/inventories/vagrant" 
end 
! 
end 
VA G R A N T 
INVOKES THE 
PROVISIONER
THESTEPS REQUIRED FOR 
END-TO-ENDTESTS 
BUILD & PACKAGE 
Package the app for production-like deployment 
VIRTUAL MACHINE 
Fire-up a local VM instance with production-like env 
CONFIGURATION & DEPLOYMENT 
Run production config and deployment code, 
having our entire system on one box 
0 
1 
3 RUN TESTS 
Execute end-to-end tests against the VM 
2
THESTEPS REQUIRED FOR 
END-TO-ENDTESTS 
BUILD & PACKAGE 
Package the app for production-like deployment 
VIRTUAL MACHINE 
Fire-up a local VM instance with production-like env 
CONFIGURATION & DEPLOYMENT 
Run production config and deployment code, 
having our entire system on one box 
0 
1 
3 RUN TESTS 
Execute end-to-end tests against the VM 
2 
CHECK OUT 
CO D E E X A M P L E
THESTEPS REQUIRED FOR 
END-TO-ENDTESTS 
BUILD & PACKAGE 
0 Package the app for production-like deployment 
2 CONFIGURATION & DEPLOYMENT 
Run production config and deployment code, 
having our entire system on one box 
1 
VIRTUAL MACHINE 
Fire-up a local VM instance with production-like env 
3 RUN TESTS 
Execute end-to-end tests against the VM
Finally, 
TESTS 
THEMSELVES 
WRITING THE
SOMEGUIDELINES 
FOR effectiveE2E TESTS 
TEST SPARINGLY 
End-to-End tests are slow; test few “sunny-day” 
scenarios 
! 
ABSTRACTION 
Tests validate features and business logic, not 
implementation details 
! 
AVOID COUPLING 
Tests should exercise the system from the 
outside without reaching for its guts
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
}
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
USING 
SPECS2 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
}
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
}
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
} 
TESTING THE 
SYSTEM BY 
USING ITSELF
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
TEST IS DECOUPLED FROM 
THE CONTROLLER API 
ITSELF BY USING DRIVERS 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
}
Drivers: 
ABSTRACTING THE SUT 
Interact with the SUT directly 
instead of the test 
Coupled to the API/protocol 
Run same test with different drivers
Drivers: 
ABSTRACTING THE SUT 
trait NotesControllerDriver { 
! 
def anAddNoteRequest = AddNoteRequest() 
! 
case class AddNoteRequest(text: String = "Lorem ipsum") 
extends Request[AddNoteResponse] { 
! 
val method = HttpMethod.POST 
val path = "/notes" 
val params: Map[String, String] = Map() 
val headers: Map[String, String] = Map() 
val body: Option[String] = Some("{ "text": "" + text + "" }") 
! 
def withText(text: String) = copy(text = text) 
} 
... 
}
Drivers: 
ABSTRACTING THE SUT 
trait NotesControllerDriver { 
! 
def anAddNoteRequest = AddNoteRequest() 
! 
case class AddNoteRequest(text: String = "Lorem ipsum") 
extends Request[AddNoteResponse] { 
! 
val method = HttpMethod.POST 
val path = "/notes" 
val params: Map[String, String] = Map() 
val headers: Map[String, String] = Map() 
val body: Option[String] = Some("{ "text": "" + text + "" }") 
! 
def withText(text: String) = copy(text = text) 
} 
... 
} 
ONLY THE DRIVER 
INTIMATELY KNOWS 
THE CONTROLLER
Drivers: 
ABSTRACTING THE SUT 
trait NotesControllerDriver { 
! 
def anAddNoteRequest = AddNoteRequest() 
! 
case class AddNoteRequest(text: String = "Lorem ipsum") 
extends Request[AddNoteResponse] { 
! 
val method = HttpMethod.POST 
val path = "/notes" 
val params: Map[String, String] = Map() 
val headers: Map[String, String] = Map() 
val body: Option[String] = Some("{ "text": "" + text + "" }") 
! 
def withText(text: String) = copy(text = text) 
} 
... 
} 
ONLY THE DRIVER 
INTIMATELY KNOWS 
THE CONTROLLER
Drivers: 
ABSTRACTING THE SUT 
trait NotesControllerDriver { 
! 
def anAddNoteRequest = AddNoteRequest() 
! 
case class AddNoteRequest(text: String = "Lorem ipsum") 
extends Request[AddNoteResponse] { 
! 
val method = HttpMethod.POST 
val path = "/notes" 
val params: Map[String, String] = Map() 
val headers: Map[String, String] = Map() 
val body: Option[String] = Some("{ "text": "" + text + "" }") 
! 
def withText(text: String) = copy(text = text) 
} 
... 
} 
ONLY THE DRIVER 
INTIMATELY KNOWS 
THE CONTROLLER
Drivers: 
ABSTRACTING THE SUT 
trait NotesControllerDriver { 
! 
def anAddNoteRequest = AddNoteRequest() 
! 
case class AddNoteRequest(text: String = "Lorem ipsum") 
extends Request[AddNoteResponse] { 
! 
val method = HttpMethod.POST 
val path = "/notes" 
val params: Map[String, String] = Map() 
val headers: Map[String, String] = Map() 
val body: Option[String] = Some("{ "text": "" + text + "" }") 
! 
def withText(text: String) = copy(text = text) 
} 
... 
} 
ONLY THE DRIVER 
INTIMATELY KNOWS 
THE CONTROLLER 
class AddNoteResponse(response: Response) 
extends BaseResponse(response) 
with JsonResponse { 
! 
lazy val noteId = (json  "noteId").extract[String] 
}
Drivers: 
ABSTRACTING THE SUT 
trait NotesControllerDriver { 
! 
def anAddNoteRequest = AddNoteRequest() 
! 
case class AddNoteRequest(text: String = "Lorem ipsum") 
extends Request[AddNoteResponse] { 
! 
val method = HttpMethod.POST 
val path = "/notes" 
val params: Map[String, String] = Map() 
val headers: Map[String, String] = Map() 
val body: Option[String] = Some("{ "text": "" + text + "" }") 
! 
def withText(text: String) = copy(text = text) 
} 
... 
} 
ONLY THE DRIVER 
INTIMATELY KNOWS 
THE CONTROLLER 
class AddNoteResponse(response: Response) 
extends BaseResponse(response) 
with JsonResponse { 
! 
lazy val noteId = (json  "noteId").extract[String] 
} 
ONLY THE DRIVER 
KNOWS THE 
RESPONSE IS JSON
Drivers: 
ABSTRACTING THE SUT 
trait NotesControllerDriver { 
! 
def anAddNoteRequest = AddNoteRequest() 
! 
case class AddNoteRequest(text: String = "Lorem ipsum") 
extends Request[AddNoteResponse] { 
! 
val method = HttpMethod.POST 
val path = "/notes" 
val params: Map[String, String] = Map() 
val headers: Map[String, String] = Map() 
val body: Option[String] = Some("{ "text": "" + text + "" }") 
! 
def withText(text: String) = copy(text = text) 
} 
... 
} 
ONLY THE DRIVER 
INTIMATELY KNOWS 
THE CONTROLLER 
class AddNoteResponse(response: Response) 
extends BaseResponse(response) 
with JsonResponse { 
! 
lazy val noteId = (json  "noteId").extract[String] 
} 
ONLY THE DRIVER 
KNOWS THE 
RESPONSE IS JSON 
OR THE FIELD 
NAME
(back to the test…)
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
}
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
}
Matchers: 
ABSTRACTING THE SUT 
The other side of the Driver, i.e: the 
response 
Help decoupling from the protocol, 
make the test be about features 
Make test more readable
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
}
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
trait ResponseMatchers extends Matchers { 
def intToIntMatcher(t: Int): Matcher[Int] = beEqualTo(t) 
private implicit ! 
def beOk = haveStatus(200) 
def beBadRequest "add a note and = haveStatus(then successfully 400) 
def haveStatus(404) 
get it" in { 
! 
beNotFound = ! 
= 
def haveStatus(status: Matcher[Int]): Matcher[Response] val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
((_: Response).status) ^^ status 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
} 
}
Memento: 
NOTES CONTROLLERTEST 
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
"Notes controller" should { 
"add a note and then successfully get it" in { 
! 
val addResp = anAddNoteRequest.withText("Hello world!").execute() 
addResp must beOk 
addResp.noteId must not beEmpty 
! 
val getResp = aGetNoteRequest.withId(addResp.noteId).execute() 
getResp must beOk 
getResp.text must_== "Hello world!" 
} 
} 
}
LOAD 
BALANCER 
HAPROXY 
REVERSE 
PROXY 
NGINX 
APP 
SERVER 
FINATRA 
DATASTORE 
ELASTICSEARCH 
TRANSLATION 
SERVICE 
YANDEX
LOAD 
BALANCER 
HAPROXY 
REVERSE 
PROXY 
NGINX 
APP 
SERVER 
FINATRA 
DATASTORE 
ELASTICSEARCH 
TRANSLATION 
SERVICE 
YANDEX
TESTING 
EXTERNAL 
SERVICES
As always, 
LIFE IS ABOUT 
COMPROMISES 
AND SETTING 
BOUNDARIES
As always, 
LIFE IS ABOUT 
COMPROMISES 
AND SETTING 
BOUNDARIES 
=> USE FAKES
FAKES FOR 
USE 
EXTERNAL SERVICES NOT EASILY AVAILABLE(Sometimes called 
Simplicators) 
Avoid making external network calls in tests 
Create Fakes based on available documentation 
Fakes should have minimal implementation to 
only support testing 
Test the Fake against the real service in a 
contract test that’s manually run
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
private val port = 9921 
private val server = new FakeYandexTranslateServer(port) 
! 
step { 
server.start() 
} 
! 
"Notes controller" should { 
"add a note and then get it translated" in { 
... 
val translated = aTranslateRequest.withId(noteId).withLang("es").execute() 
translated must beOk 
translated.text must_== "Buenos días" 
} 
} 
! 
step { 
server.stop() 
} 
}
class NotesControllerEndToEndTest 
extends Specification 
with NotesControllerDriver 
with ResponseMatchers { 
! 
private val port = 9921 
private val server = new FakeYandexTranslateServer(port) 
! 
step { 
class FakeYandexTranslateServer(port: Int) extends SimpleHttpServer(port) { 
! 
private val json = 
server.start() 
} 
! 
"Notes controller" should { 
"""{ 
|"code": 200, 
|"lang": "en-es", 
|"text": ["Buenos días"] 
|} 
""".stripMargin 
"add a note and then get it translated" in { 
... 
val translated = aTranslateRequest.withId(noteId).withLang("es").execute() 
translated must beOk 
translated.text must_== "Buenos días" 
} 
} 
! 
step { 
! 
override protected def onSimpleRequest(request: HttpRequest): String = json 
} 
server.stop() 
} 
}
LOAD 
BALANCER 
HAPROXY 
REVERSE 
PROXY 
NGINX 
APP 
SERVER 
FINATRA 
DATASTORE 
ELASTICSEARCH 
TRANSLATION 
SERVICE 
YANDEX
LOAD 
BALANCER 
HAPROXY 
REVERSE 
PROXY 
NGINX 
APP 
SERVER 
FINATRA 
DATASTORE 
ELASTICSEARCH 
TRANSLATION 
SERVICE 
YANDEX
WHAT YOU 
SHOULD 
DO NEXT
How to 
CONTINUE 
FROM HERE 
CHECK OUT THE EXAMPLE 
github.com/orrsella/scala-e2e-testing 
! 
END-TO-END TEST NEW PROJECTS 
It is easier to get started with e2e tests on a new 
project, integrating into an existing one is harder 
! 
READ 
Growing Object-Oriented Software Guided by Tests
How to 
CONTINUE 
FROM HERE 
CHECK OUT THE EXAMPLE 
github.com/orrsella/scala-e2e-testing 
! 
END-TO-END TEST NEW PROJECTS 
It is easier to get started with e2e tests on a new 
project, integrating into an existing one is harder 
! 
READ 
Growing Object-Oriented Software Guided by Tests
THANK 
YOU
True End-to-End Testing in Scala
True End-to-End Testing in Scala

More Related Content

Viewers also liked

Testing practicies not only in scala
Testing practicies not only in scalaTesting practicies not only in scala
Testing practicies not only in scalaPaweł Panasewicz
 
Benefit From Unit Testing In The Real World
Benefit From Unit Testing In The Real WorldBenefit From Unit Testing In The Real World
Benefit From Unit Testing In The Real WorldDror Helper
 
User Testing by Example
User Testing by ExampleUser Testing by Example
User Testing by ExampleJeremy Horn
 
Agile testing and_the_banking_domain_2009
Agile testing and_the_banking_domain_2009Agile testing and_the_banking_domain_2009
Agile testing and_the_banking_domain_2009Anil Kumar
 
Advanced unit testing – real life examples and mistakes
Advanced unit testing – real life examples and mistakesAdvanced unit testing – real life examples and mistakes
Advanced unit testing – real life examples and mistakesMilan Vukoje
 
Is an agile SDLC an oxymoron?
Is an agile SDLC an oxymoron? Is an agile SDLC an oxymoron?
Is an agile SDLC an oxymoron? Dave Sharrock
 
Testing of e-Banking - Case Study
Testing of e-Banking - Case Study Testing of e-Banking - Case Study
Testing of e-Banking - Case Study OAK Systems Pvt Ltd
 
Practical type mining in Scala
Practical type mining in ScalaPractical type mining in Scala
Practical type mining in ScalaRose Toomey
 
Linking Upstream and Downstream Agile
Linking Upstream and Downstream AgileLinking Upstream and Downstream Agile
Linking Upstream and Downstream AgileCollabNet
 
End-2-End Monitoring – Der Prüfstand jedes SLA´s – in 15 Minuten erklärt!
End-2-End Monitoring – Der Prüfstand jedes SLA´s – in 15 Minuten erklärt!End-2-End Monitoring – Der Prüfstand jedes SLA´s – in 15 Minuten erklärt!
End-2-End Monitoring – Der Prüfstand jedes SLA´s – in 15 Minuten erklärt!MAXXYS AG
 
Effective Scala: Programming Patterns
Effective Scala: Programming PatternsEffective Scala: Programming Patterns
Effective Scala: Programming PatternsVasil Remeniuk
 
Akselos solutions for oil & gas
Akselos solutions for oil & gasAkselos solutions for oil & gas
Akselos solutions for oil & gasAlonso Giannoni
 

Viewers also liked (14)

Testing practicies not only in scala
Testing practicies not only in scalaTesting practicies not only in scala
Testing practicies not only in scala
 
Benefit From Unit Testing In The Real World
Benefit From Unit Testing In The Real WorldBenefit From Unit Testing In The Real World
Benefit From Unit Testing In The Real World
 
User Testing by Example
User Testing by ExampleUser Testing by Example
User Testing by Example
 
QA Tester Junior
QA Tester JuniorQA Tester Junior
QA Tester Junior
 
Agile testing and_the_banking_domain_2009
Agile testing and_the_banking_domain_2009Agile testing and_the_banking_domain_2009
Agile testing and_the_banking_domain_2009
 
Advanced unit testing – real life examples and mistakes
Advanced unit testing – real life examples and mistakesAdvanced unit testing – real life examples and mistakes
Advanced unit testing – real life examples and mistakes
 
Is an agile SDLC an oxymoron?
Is an agile SDLC an oxymoron? Is an agile SDLC an oxymoron?
Is an agile SDLC an oxymoron?
 
Browser-level testing
Browser-level testingBrowser-level testing
Browser-level testing
 
Testing of e-Banking - Case Study
Testing of e-Banking - Case Study Testing of e-Banking - Case Study
Testing of e-Banking - Case Study
 
Practical type mining in Scala
Practical type mining in ScalaPractical type mining in Scala
Practical type mining in Scala
 
Linking Upstream and Downstream Agile
Linking Upstream and Downstream AgileLinking Upstream and Downstream Agile
Linking Upstream and Downstream Agile
 
End-2-End Monitoring – Der Prüfstand jedes SLA´s – in 15 Minuten erklärt!
End-2-End Monitoring – Der Prüfstand jedes SLA´s – in 15 Minuten erklärt!End-2-End Monitoring – Der Prüfstand jedes SLA´s – in 15 Minuten erklärt!
End-2-End Monitoring – Der Prüfstand jedes SLA´s – in 15 Minuten erklärt!
 
Effective Scala: Programming Patterns
Effective Scala: Programming PatternsEffective Scala: Programming Patterns
Effective Scala: Programming Patterns
 
Akselos solutions for oil & gas
Akselos solutions for oil & gasAkselos solutions for oil & gas
Akselos solutions for oil & gas
 

Recently uploaded

Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...MyIntelliSource, Inc.
 
Right Money Management App For Your Financial Goals
Right Money Management App For Your Financial GoalsRight Money Management App For Your Financial Goals
Right Money Management App For Your Financial GoalsJhone kinadey
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Steffen Staab
 
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...MyIntelliSource, Inc.
 
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AISyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AIABDERRAOUF MEHENNI
 
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...harshavardhanraghave
 
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online ☂️
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online  ☂️CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online  ☂️
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online ☂️anilsa9823
 
How To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.jsHow To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.jsAndolasoft Inc
 
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...panagenda
 
Optimizing AI for immediate response in Smart CCTV
Optimizing AI for immediate response in Smart CCTVOptimizing AI for immediate response in Smart CCTV
Optimizing AI for immediate response in Smart CCTVshikhaohhpro
 
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfThe Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfkalichargn70th171
 
Unveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Unveiling the Tech Salsa of LAMs with Janus in Real-Time ApplicationsUnveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Unveiling the Tech Salsa of LAMs with Janus in Real-Time ApplicationsAlberto González Trastoy
 
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...kellynguyen01
 
A Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docxA Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docxComplianceQuest1
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providermohitmore19
 
5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdfWave PLM
 
HR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.comHR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.comFatema Valibhai
 

Recently uploaded (20)

Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
 
Right Money Management App For Your Financial Goals
Right Money Management App For Your Financial GoalsRight Money Management App For Your Financial Goals
Right Money Management App For Your Financial Goals
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
 
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
 
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AISyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
 
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICECHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
 
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
 
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online ☂️
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online  ☂️CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online  ☂️
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online ☂️
 
How To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.jsHow To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.js
 
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
W01_panagenda_Navigating-the-Future-with-The-Hitchhikers-Guide-to-Notes-and-D...
 
Optimizing AI for immediate response in Smart CCTV
Optimizing AI for immediate response in Smart CCTVOptimizing AI for immediate response in Smart CCTV
Optimizing AI for immediate response in Smart CCTV
 
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfThe Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
 
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS LiveVip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
 
Unveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Unveiling the Tech Salsa of LAMs with Janus in Real-Time ApplicationsUnveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Unveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
 
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
 
A Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docxA Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docx
 
Microsoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdfMicrosoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdf
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
 
5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf
 
HR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.comHR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.com
 

True End-to-End Testing in Scala

  • 1. TRUE END-TO-END TESTING SCALA Orr Sella IN Scalapeño 2014
  • 2. AGENDA WHAT ARE THEY WHY WE NEED THEM HOW TO WRITE THEM END-TO-END TESTS TEST HARNESS DRIVERS FAKES …
  • 3. AGENDA WHAT ARE THEY WHY WE NEED THEM HOW TO WRITE THEM END-TO-END TESTS TEST HARNESS DRIVERS FAKES … CO N C R E T E E X A M P L E github.com/orrsella/scala-e2e-testing
  • 4. Disclaimer BIG SUBJECT, WE CAN’T COVER EVERYTHING
  • 5. Why SCALA & END-TO-END TESTS 1 THE APPS Typical Scala applications are ripe for end-to-end tests ! THE LANGUAGE & ECOSYSTEM Build tools, testing libraries and language features are a great fit 2
  • 7. The Bible: GOOS INTRO TO TDD ! TESTING TECHNIQUES ! LOTS OF CODE & EXAMPLES
  • 8. THELEVELS OF TESTING KNOWN TO MAN 1 UNIT Do our objects do the right thing? ! INTEGRATION Does our code work against code we can’t change? ! END-TO-END Does the whole system work? 2 3 (not really)
  • 9. WHAT IS THE RIGHT BALANCE OF THE THREE?
  • 10. THEINVERSE ICE CREAMCONE(or pyramid) NUMBER OF TESTS
  • 11. THEINVERSE ICE CREAMCONE(or pyramid) END-TO- END INTEGRATION UNIT NUMBER OF TESTS
  • 12. THEINVERSE ICE CREAMCONE(or pyramid) END-TO- END INTEGRATION UNIT MANUAL TESTING (if you must) NUMBER OF TESTS
  • 14. END-TO-END TESTS Interact with the system from the outside Black Box MOST IMPORTANTLY: Exercising both the system and the process by which it’s built and deployed Include interaction with external environment
  • 15. AN IDEALAUTOMATIC BUILDWOULD 1 Compile and unit-test the code Integrate and package the system Perform a production-like deployment to a realistic environment Exercise the system through its external access points 2 3 4
  • 16. AN IDEALAUTOMATIC BUILDWOULD 1 Compile and unit-test the code Integrate and package the system Perform a production-like deployment to a realistic environment Exercise the system through its external access points 2 3 4
  • 17. Whoa, THIS SOUNDS LIKE A LOT OF WORK
  • 18. Whoa, THIS SOUNDS LIKE A LOT OF WORK IT IS.
  • 19. But, IS IT WORTH IT?
  • 20. But, IS IT WORTH IT? YES. (I hope to convince you)
  • 21. WHY ARE END-TO-END TESTS SO IMPORTANT?
  • 22. HERE ARE3 MAIN (many more...) REASONS
  • 23. 1 FLUSH OUT THE UNKNOWNS Requires asking (and answering) many awkward questions Expose uncertainty early, including technical and organizational risks Forces to understand how a system fits into the world Identify all the integration (read: potential failure) points
  • 24. 1 FLUSH OUT THE UNKNOWNS Requires asking (and answering) many awkward questions Expose uncertainty early, including technical and organizational risks Forces to understand how a system fits into the world Identify all the integration (read: potential failure) points EXAMPLE IN 3 SLIDES
  • 25. 2 FEEDBACK & CONFIDENCE Feedback that would otherwise only show in staging/production Safety net of system-wide regression Assurance that our chosen technology stack (“plumbing”) works as expected Confidence to make “risky” system-wide changes (e.g: swap datastore, application server)
  • 26. 3 FORCED AUTOMATION Everything must be automated so it can be tested Crucial for deployment which is error-prone Automatic Build-Deploy-Test cycle means we can ship frequently
  • 27. TIME FOR A CONCRETE EXAMPLE:
  • 28. TIME FOR A CONCRETE EXAMPLE: Memento
  • 29. TIME FOR A CONCRETE EXAMPLE: Memento github.com/orrsella/scala-e2e-testing
  • 31. Memento? NOTE-TAKING WEB SERVICE STOR E, RE TRIEVE, SEARC H, TRANSLATE
  • 32. Memento? NOTE-TAKING WEB SERVICE STOR E, RE TRIEVE, SEARC H, TRANSLATE “MICROSERVICE”
  • 33. Memento? NOTE-TAKING WEB SERVICE STOR E, RE TRIEVE, SEARC H, TRANSLATE “MICROSERVICE”REST API
  • 34. LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX
  • 35. LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX SCALA!..
  • 36. LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX SCALA!.. PRETTY STRAIGHTFORWARD
  • 37. Ok. So. WHAT CAN POSSIBLY GO WRONG & BE MISCONFIGURED? i.e. integration (failure) points
  • 38. LOAD BALANCER HAPROXY PORT 80 /ETC/HAPROXY/ HAPROXY.CFG LIST OF REVERSE PROXIES:PORTS /ETC/DEFAULT/ HAPROXY
  • 39. REVERSE PROXY NGINX APP SERVER FINATRA /ETC/NGINX/ NGINX.CONF PROXY PORT 7770 INIT.D SCRI P T /ETC/NGINX/ SITES-AVAILABLE APPLICATION.CONF LO G BAC K . XM L JAVA DEPLOY NEW VERSION
  • 40. DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX PORT 9300 MAPPING URL API KEY /ETC/ELSATICSEARCH.YML JAVA
  • 41. (just a partial list…)
  • 42. Ok, SO A LOT CAN (read: will) GO WRONG
  • 43. Ok, SO A LOT CAN (read: will) GO WRONG WE WANT OUR END-TO-END TESTS TO E X E RC I S E A L L O F T H I S !
  • 44. HOW DO WE TEST ALL OF THIS END-TO-END?
  • 45. HOW DO WE TEST ALL OF THIS END-TO-END? here is one approach…
  • 46. HOW DO WE TEST ALL OF THIS END-TO-END? here is one approach… THERE ARE OT H E R S
  • 47. HOW DO WE TEST ALL OF THIS END-TO-END? here is one approach… THERE ARE OT H E R S BUT THIS IS THE BEST, OBVIOUSLY
  • 48. STEPSREQUIRED FOR END-TO-ENDTESTS 0 1 BUILD & PACKAGE Package the app for production deployment 2 3 VIRTUAL MACHINE Fire-up a local VM instance with production-like env CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box RUN TESTS Execute end-to-end tests against the VM
  • 49. HOW DO WE ORCHESTRATE ALL OF THIS?
  • 50. Enter: THE TEST HARNESS EXTENSION OF THE BUILD Runs all the necessary setup/teardown actions and the tests themselves ! (aka sbt) STANDARD/SIMPLE/SCALA BUILD TOOL Build and package our app (sbt-native-packager) Fire-up the virtual machine (Vagrant) Run configuration and deployment scripts (Ansible)
  • 51. Meet VAGRANT PORTABLE DEV ENVIRONMENTS Lightweight, reproducible and programmable ! VIRTUALIZATION Wrapper around VirtualBox, VMWare, more ! SIMPLE & QUICK Vagrantfile => `$ vagrant up`
  • 52. THESTEPS REQUIRED FOR END-TO-ENDTESTS BUILD & PACKAGE Package the app for production-like deployment 0 1 VIRTUAL MACHINE Fire-up a local VM instance with production-like env 2 CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box 3 RUN TESTS Execute end-to-end tests against the VM
  • 53. Test Harness: SBT MANAGING VAGRANT // project/Vagrant.scala ! object Vagrant { ! private lazy val vagrant = settingKey[Vagrant]("vagrant") ! lazy val settings = Seq( test in EndToEndTest <<= (test in EndToEndTest).dependsOn(publishLocal), testOptions in EndToEndTest += Tests.Setup(() => vagrant.value.setup()), testOptions in EndToEndTest += Tests.Cleanup(() => vagrant.value.cleanup()) ) }
  • 54. Test Harness: SBT MANAGING VAGRANT // project/Vagrant.scala ! object Vagrant { ! private lazy val vagrant = settingKey[Vagrant]("vagrant") ! lazy val settings = Seq( test in EndToEndTest <<= (test in EndToEndTest).dependsOn(publishLocal), testOptions in EndToEndTest += Tests.Setup(() => vagrant.value.setup()), testOptions in EndToEndTest += Tests.Cleanup(() => vagrant.value.cleanup()) ) } TEARDOWN HOOK SETUP HOOK
  • 55. Test Harness: SBT MANAGING VAGRANT class Vagrant(vagrantFile: File) { ! // cli method wrappers private def up() = Process("vagrant" :: "up" :: Nil, dir)! private def provision() = Process("vagrant" :: "provision" :: Nil, dir)! ! def setup(): Unit = { prevStatus = status() prevStatus match { case Running => provision() case Saved => up(); provision() case NotCreated => up() case Unknown => up() } } ! def cleanup(): Unit = if (prevStatus != Running) suspend() }
  • 56. Test Harness: SBT MANAGING VAGRANT class Vagrant(vagrantFile: File) { ! // cli method wrappers private def up() = Process("vagrant" :: "up" :: Nil, dir)! private def provision() = Process("vagrant" :: "provision" :: Nil, dir)! ! def setup(): Unit = { prevStatus = status() prevStatus match { case Running => provision() case Saved => up(); provision() case NotCreated => up() case Unknown => up() } } ! def cleanup(): Unit = if (prevStatus != Running) suspend() }
  • 57. Test Harness: SBT MANAGING VAGRANT class Vagrant(vagrantFile: File) { ! // cli method wrappers private def up() = Process("vagrant" :: "up" :: Nil, dir)! private def provision() = Process("vagrant" :: "provision" :: Nil, dir)! ! def setup(): Unit = { Tip: VA G R A N T S TAT E “J U G G L I N G ” , S AV E S A L O T prevStatus = status() prevStatus match { case Running => provision() case Saved => up(); provision() case NotCreated => up() case Unknown => up() } } ! def cleanup(): Unit = if (prevStatus != Running) suspend() } OF TIME!
  • 58. THESTEPS REQUIRED FOR END-TO-ENDTESTS 0 1 BUILD & PACKAGE Package the app for production-like deployment 2 CONFIGURATION & DEPLOYMENT 3 VIRTUAL MACHINE Fire-up a local VM instance with production-like env Run production config and deployment code, having our entire system on one box RUN TESTS Execute end-to-end tests against the VM
  • 59. THEDEPLOYMENT SCRIPTS (Provision in Vagrant parlance) CONFIGURATION MANAGEMENT Ensure that our system is configured properly (OS, settings, packages, file system, etc.) ! DEPLOY THE APP Upgrade to the latest version just compiled and packaged, as would be done in production ! APPLICATION CONFIG Configure the application itself the same way it would in production (but with test values)
  • 60. Specifically, ANSIBLE BUT ANY CM TOOL WILL DO (Chef, Puppet, Shell scripts)
  • 61. Provision: VAGRANTFILE Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| ! config.vm.box = "ubuntu/trusty64" config.vm.network "forwarded_port", guest: 7770, host: 7769 ... ! config.vm.provision "ansible" do |ansible| ansible.playbook = "ansible/site.yml" ansible.inventory_path = "ansible/inventories/vagrant" end ! end
  • 62. Provision: VAGRANTFILE Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| ! config.vm.box = "ubuntu/trusty64" config.vm.network "forwarded_port", guest: 7770, host: 7769 ... ! config.vm.provision "ansible" do |ansible| ansible.playbook = "ansible/site.yml" ansible.inventory_path = "ansible/inventories/vagrant" end ! end
  • 63. Provision: VAGRANTFILE Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| ! config.vm.box = "ubuntu/trusty64" config.vm.network "forwarded_port", guest: 7770, host: 7769 ... ! config.vm.provision "ansible" do |ansible| ansible.playbook = "ansible/site.yml" ansible.inventory_path = "ansible/inventories/vagrant" end ! end VA G R A N T INVOKES THE PROVISIONER
  • 64. THESTEPS REQUIRED FOR END-TO-ENDTESTS BUILD & PACKAGE Package the app for production-like deployment VIRTUAL MACHINE Fire-up a local VM instance with production-like env CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box 0 1 3 RUN TESTS Execute end-to-end tests against the VM 2
  • 65. THESTEPS REQUIRED FOR END-TO-ENDTESTS BUILD & PACKAGE Package the app for production-like deployment VIRTUAL MACHINE Fire-up a local VM instance with production-like env CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box 0 1 3 RUN TESTS Execute end-to-end tests against the VM 2 CHECK OUT CO D E E X A M P L E
  • 66. THESTEPS REQUIRED FOR END-TO-ENDTESTS BUILD & PACKAGE 0 Package the app for production-like deployment 2 CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box 1 VIRTUAL MACHINE Fire-up a local VM instance with production-like env 3 RUN TESTS Execute end-to-end tests against the VM
  • 68. SOMEGUIDELINES FOR effectiveE2E TESTS TEST SPARINGLY End-to-End tests are slow; test few “sunny-day” scenarios ! ABSTRACTION Tests validate features and business logic, not implementation details ! AVOID COUPLING Tests should exercise the system from the outside without reaching for its guts
  • 69. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } }
  • 70. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { USING SPECS2 "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } }
  • 71. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } }
  • 72. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } } TESTING THE SYSTEM BY USING ITSELF
  • 73. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { TEST IS DECOUPLED FROM THE CONTROLLER API ITSELF BY USING DRIVERS "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } }
  • 74. Drivers: ABSTRACTING THE SUT Interact with the SUT directly instead of the test Coupled to the API/protocol Run same test with different drivers
  • 75. Drivers: ABSTRACTING THE SUT trait NotesControllerDriver { ! def anAddNoteRequest = AddNoteRequest() ! case class AddNoteRequest(text: String = "Lorem ipsum") extends Request[AddNoteResponse] { ! val method = HttpMethod.POST val path = "/notes" val params: Map[String, String] = Map() val headers: Map[String, String] = Map() val body: Option[String] = Some("{ "text": "" + text + "" }") ! def withText(text: String) = copy(text = text) } ... }
  • 76. Drivers: ABSTRACTING THE SUT trait NotesControllerDriver { ! def anAddNoteRequest = AddNoteRequest() ! case class AddNoteRequest(text: String = "Lorem ipsum") extends Request[AddNoteResponse] { ! val method = HttpMethod.POST val path = "/notes" val params: Map[String, String] = Map() val headers: Map[String, String] = Map() val body: Option[String] = Some("{ "text": "" + text + "" }") ! def withText(text: String) = copy(text = text) } ... } ONLY THE DRIVER INTIMATELY KNOWS THE CONTROLLER
  • 77. Drivers: ABSTRACTING THE SUT trait NotesControllerDriver { ! def anAddNoteRequest = AddNoteRequest() ! case class AddNoteRequest(text: String = "Lorem ipsum") extends Request[AddNoteResponse] { ! val method = HttpMethod.POST val path = "/notes" val params: Map[String, String] = Map() val headers: Map[String, String] = Map() val body: Option[String] = Some("{ "text": "" + text + "" }") ! def withText(text: String) = copy(text = text) } ... } ONLY THE DRIVER INTIMATELY KNOWS THE CONTROLLER
  • 78. Drivers: ABSTRACTING THE SUT trait NotesControllerDriver { ! def anAddNoteRequest = AddNoteRequest() ! case class AddNoteRequest(text: String = "Lorem ipsum") extends Request[AddNoteResponse] { ! val method = HttpMethod.POST val path = "/notes" val params: Map[String, String] = Map() val headers: Map[String, String] = Map() val body: Option[String] = Some("{ "text": "" + text + "" }") ! def withText(text: String) = copy(text = text) } ... } ONLY THE DRIVER INTIMATELY KNOWS THE CONTROLLER
  • 79. Drivers: ABSTRACTING THE SUT trait NotesControllerDriver { ! def anAddNoteRequest = AddNoteRequest() ! case class AddNoteRequest(text: String = "Lorem ipsum") extends Request[AddNoteResponse] { ! val method = HttpMethod.POST val path = "/notes" val params: Map[String, String] = Map() val headers: Map[String, String] = Map() val body: Option[String] = Some("{ "text": "" + text + "" }") ! def withText(text: String) = copy(text = text) } ... } ONLY THE DRIVER INTIMATELY KNOWS THE CONTROLLER class AddNoteResponse(response: Response) extends BaseResponse(response) with JsonResponse { ! lazy val noteId = (json "noteId").extract[String] }
  • 80. Drivers: ABSTRACTING THE SUT trait NotesControllerDriver { ! def anAddNoteRequest = AddNoteRequest() ! case class AddNoteRequest(text: String = "Lorem ipsum") extends Request[AddNoteResponse] { ! val method = HttpMethod.POST val path = "/notes" val params: Map[String, String] = Map() val headers: Map[String, String] = Map() val body: Option[String] = Some("{ "text": "" + text + "" }") ! def withText(text: String) = copy(text = text) } ... } ONLY THE DRIVER INTIMATELY KNOWS THE CONTROLLER class AddNoteResponse(response: Response) extends BaseResponse(response) with JsonResponse { ! lazy val noteId = (json "noteId").extract[String] } ONLY THE DRIVER KNOWS THE RESPONSE IS JSON
  • 81. Drivers: ABSTRACTING THE SUT trait NotesControllerDriver { ! def anAddNoteRequest = AddNoteRequest() ! case class AddNoteRequest(text: String = "Lorem ipsum") extends Request[AddNoteResponse] { ! val method = HttpMethod.POST val path = "/notes" val params: Map[String, String] = Map() val headers: Map[String, String] = Map() val body: Option[String] = Some("{ "text": "" + text + "" }") ! def withText(text: String) = copy(text = text) } ... } ONLY THE DRIVER INTIMATELY KNOWS THE CONTROLLER class AddNoteResponse(response: Response) extends BaseResponse(response) with JsonResponse { ! lazy val noteId = (json "noteId").extract[String] } ONLY THE DRIVER KNOWS THE RESPONSE IS JSON OR THE FIELD NAME
  • 82. (back to the test…)
  • 83. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } }
  • 84. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } }
  • 85. Matchers: ABSTRACTING THE SUT The other side of the Driver, i.e: the response Help decoupling from the protocol, make the test be about features Make test more readable
  • 86. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } }
  • 87. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { trait ResponseMatchers extends Matchers { def intToIntMatcher(t: Int): Matcher[Int] = beEqualTo(t) private implicit ! def beOk = haveStatus(200) def beBadRequest "add a note and = haveStatus(then successfully 400) def haveStatus(404) get it" in { ! beNotFound = ! = def haveStatus(status: Matcher[Int]): Matcher[Response] val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! ((_: Response).status) ^^ status val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } } }
  • 88. Memento: NOTES CONTROLLERTEST class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! "Notes controller" should { "add a note and then successfully get it" in { ! val addResp = anAddNoteRequest.withText("Hello world!").execute() addResp must beOk addResp.noteId must not beEmpty ! val getResp = aGetNoteRequest.withId(addResp.noteId).execute() getResp must beOk getResp.text must_== "Hello world!" } } }
  • 89. LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX
  • 90. LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX
  • 92. As always, LIFE IS ABOUT COMPROMISES AND SETTING BOUNDARIES
  • 93. As always, LIFE IS ABOUT COMPROMISES AND SETTING BOUNDARIES => USE FAKES
  • 94. FAKES FOR USE EXTERNAL SERVICES NOT EASILY AVAILABLE(Sometimes called Simplicators) Avoid making external network calls in tests Create Fakes based on available documentation Fakes should have minimal implementation to only support testing Test the Fake against the real service in a contract test that’s manually run
  • 95. class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! private val port = 9921 private val server = new FakeYandexTranslateServer(port) ! step { server.start() } ! "Notes controller" should { "add a note and then get it translated" in { ... val translated = aTranslateRequest.withId(noteId).withLang("es").execute() translated must beOk translated.text must_== "Buenos días" } } ! step { server.stop() } }
  • 96. class NotesControllerEndToEndTest extends Specification with NotesControllerDriver with ResponseMatchers { ! private val port = 9921 private val server = new FakeYandexTranslateServer(port) ! step { class FakeYandexTranslateServer(port: Int) extends SimpleHttpServer(port) { ! private val json = server.start() } ! "Notes controller" should { """{ |"code": 200, |"lang": "en-es", |"text": ["Buenos días"] |} """.stripMargin "add a note and then get it translated" in { ... val translated = aTranslateRequest.withId(noteId).withLang("es").execute() translated must beOk translated.text must_== "Buenos días" } } ! step { ! override protected def onSimpleRequest(request: HttpRequest): String = json } server.stop() } }
  • 97. LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX
  • 98. LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX
  • 99. WHAT YOU SHOULD DO NEXT
  • 100. How to CONTINUE FROM HERE CHECK OUT THE EXAMPLE github.com/orrsella/scala-e2e-testing ! END-TO-END TEST NEW PROJECTS It is easier to get started with e2e tests on a new project, integrating into an existing one is harder ! READ Growing Object-Oriented Software Guided by Tests
  • 101. How to CONTINUE FROM HERE CHECK OUT THE EXAMPLE github.com/orrsella/scala-e2e-testing ! END-TO-END TEST NEW PROJECTS It is easier to get started with e2e tests on a new project, integrating into an existing one is harder ! READ Growing Object-Oriented Software Guided by Tests