Exploring what end-to-end tests are, why they are important for Scala apps, and how to write them. A talk given at Scalapeno 2014. Talk is accompanied by a code example: https://github.com/orrsella/scala-e2e-testing
Abstract:
It seems that by 2014 we have all come to the realization that testing is good, and today no reputable library/framework will be released without comprehensive tests. Whether you're developing using TDD or not, you're probably writing tests in some capacity. We have a plethora of testing libraries and tools that make writing tests in Scala extremely easy.
In this talk we will show how we can take our unit/integration tests a step further, and see how to test our applications end-to-end. Scala is a prime candidate for end-to-end testing, having all the right pieces in place: an easily extendable build tool (SBT), fluent DSL testing libraries (Specs2/ScalaTest), and the power/tooling of the JVM behind it.
We will see how to approach testing our application from the outside, and touch on concepts such as: the Test Harness, abstracting our SUT (System Under Test) by using test Drivers, using Simplicators for testing against external dependencies, and more. We will put everything together by using tools such as: SBT, Ansible and Vagrant.
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
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)
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
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
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
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
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)
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
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
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
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