Slide 1

Slide 1 text

TRUE END-TO-END TESTING SCALA Orr Sella IN Scalapeño 2014

Slide 2

Slide 2 text

AGENDA WHAT ARE THEY WHY WE NEED THEM HOW TO WRITE THEM END-TO-END TESTS TEST HARNESS DRIVERS FAKES …

Slide 3

Slide 3 text

AGENDA WHAT ARE THEY WHY WE NEED THEM HOW TO WRITE THEM END-TO-END TESTS TEST HARNESS DRIVERS FAKES … CONCRETE EXAMPLE github.com/orrsella/scala-e2e-testing

Slide 4

Slide 4 text

BIG SUBJECT, WE CAN’T COVER EVERYTHING Disclaimer

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

SOME TERMINOLOGY First,

Slide 7

Slide 7 text

The Bible: GOOS INTRO TO TDD ! TESTING TECHNIQUES ! LOTS OF CODE & EXAMPLES

Slide 8

Slide 8 text

LEVELS OF THE TESTINGKNOWN 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)

Slide 9

Slide 9 text

RIGHT BALANCE WHAT IS THE OF THE THREE?

Slide 10

Slide 10 text

INVERSE THE ICE CREAMCONE (or pyramid) NUMBER OF TESTS

Slide 11

Slide 11 text

INVERSE THE ICE CREAMCONE (or pyramid) END- TO-END INTEGRATION UNIT NUMBER OF TESTS

Slide 12

Slide 12 text

INVERSE THE ICE CREAMCONE (or pyramid) END- TO-END INTEGRATION UNIT MANUAL TESTING (if you must) NUMBER OF TESTS

Slide 13

Slide 13 text

END-TO-END WHAT ARE TESTS?

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

AUTOMATIC AN IDEAL 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

Slide 16

Slide 16 text

AUTOMATIC AN IDEAL 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

Slide 17

Slide 17 text

THIS SOUNDS LIKE A LOT OF WORK Whoa,

Slide 18

Slide 18 text

THIS SOUNDS LIKE A LOT OF WORK Whoa, IT IS.

Slide 19

Slide 19 text

IS IT WORTH IT? But,

Slide 20

Slide 20 text

IS IT WORTH IT? But, YES. (I hope to convince you)

Slide 21

Slide 21 text

END-TO-END WHY ARE TESTS SO IMPORTANT?

Slide 22

Slide 22 text

REASONS HERE ARE3 MAIN (many more...)

Slide 23

Slide 23 text

FLUSH OUT THE UNKNOWNS 1 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

Slide 24

Slide 24 text

FLUSH OUT THE UNKNOWNS 1 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

Slide 25

Slide 25 text

FEEDBACK & CONFIDENCE 2 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)

Slide 26

Slide 26 text

FORCED AUTOMATION 3 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

Slide 27

Slide 27 text

TIME FOR A CONCRETE EXAMPLE:

Slide 28

Slide 28 text

TIME FOR A CONCRETE EXAMPLE: Memento

Slide 29

Slide 29 text

TIME FOR A CONCRETE EXAMPLE: Memento github.com/orrsella/scala-e2e-testing

Slide 30

Slide 30 text

NOTE-TAKING WEB SERVICE Memento?

Slide 31

Slide 31 text

NOTE-TAKING WEB SERVICE Memento? STORE, RETRIEVE, SEARCH, TRANSLATE

Slide 32

Slide 32 text

NOTE-TAKING WEB SERVICE Memento? STORE, RETRIEVE, SEARCH, TRANSLATE “MICROSERVICE”

Slide 33

Slide 33 text

NOTE-TAKING WEB SERVICE Memento? STORE, RETRIEVE, SEARCH, TRANSLATE “MICROSERVICE” REST API

Slide 34

Slide 34 text

LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX

Slide 35

Slide 35 text

LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX SCALA!..

Slide 36

Slide 36 text

LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX SCALA!.. PRETTY STRAIGHTFORWARD

Slide 37

Slide 37 text

WHAT CAN POSSIBLY GO WRONG & BE MISCONFIGURED? Ok. So. i.e. integration (failure) points

Slide 38

Slide 38 text

LOAD BALANCER HAPROXY PORT 80 /ETC/HAPROXY/ HAPROXY.CFG LIST OF REVERSE PROXIES:PORTS /ETC/DEFAULT/ HAPROXY

Slide 39

Slide 39 text

REVERSE PROXY NGINX APP SERVER FINATRA PROXY PORT 7770 /ETC/NGINX/ NGINX.CONF INIT.D SCRIPT /ETC/NGINX/ SITES-AVAILABLE JAVA APPLICATION.CONF LOGBACK.XML DEPLOY NEW VERSION

Slide 40

Slide 40 text

DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX PORT 9300 MAPPING URL API KEY /ETC/ELSATICSEARCH.YML JAVA

Slide 41

Slide 41 text

(just a partial list…)

Slide 42

Slide 42 text

SO A LOT CAN GO WRONG Ok, (read: will)

Slide 43

Slide 43 text

SO A LOT CAN GO WRONG Ok, (read: will) WE WANT OUR END-TO-END TESTS TO EXERCISE ALL OF THIS!

Slide 44

Slide 44 text

HOW DO WE TEST ALL OF THIS END-TO-END?

Slide 45

Slide 45 text

HOW DO WE TEST ALL OF THIS END-TO-END? here is one approach…

Slide 46

Slide 46 text

HOW DO WE TEST ALL OF THIS END-TO-END? here is one approach… THERE ARE OTHERS

Slide 47

Slide 47 text

HOW DO WE TEST ALL OF THIS END-TO-END? here is one approach… THERE ARE OTHERS BUT THIS IS THE BEST, OBVIOUSLY

Slide 48

Slide 48 text

REQUIRED FOR STEPS END-TO-ENDTESTS 1 BUILD & PACKAGE Package the app for production deployment 2 3 0 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

Slide 49

Slide 49 text

ORCHESTRATE HOW DO WE ALL OF THIS?

Slide 50

Slide 50 text

THE TEST HARNESS Enter: EXTENSION OF THE BUILD Runs all the necessary setup/teardown actions and the tests themselves ! 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) (aka sbt)

Slide 51

Slide 51 text

VAGRANT Meet PORTABLE DEV ENVIRONMENTS Lightweight, reproducible and programmable ! VIRTUALIZATION Wrapper around VirtualBox, VMWare, more ! SIMPLE & QUICK Vagrantfile => `$ vagrant up`

Slide 52

Slide 52 text

2 CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box STEPS REQUIRED FOR THE END-TO-ENDTESTS BUILD & PACKAGE Package the app for production-like deployment 3 0 RUN TESTS Execute end-to-end tests against the VM 1 VIRTUAL MACHINE Fire-up a local VM instance with production-like env

Slide 53

Slide 53 text

Test Harness: MANAGING VAGRANT SBT // 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()) ) }

Slide 54

Slide 54 text

Test Harness: MANAGING VAGRANT SBT // 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

Slide 55

Slide 55 text

Test Harness: MANAGING VAGRANT SBT 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() }

Slide 56

Slide 56 text

Test Harness: MANAGING VAGRANT SBT 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() }

Slide 57

Slide 57 text

Test Harness: MANAGING VAGRANT SBT 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() } Tip: VAGRANT STATE “JUGGLING”, SAVES A LOT OF TIME!

Slide 58

Slide 58 text

STEPS REQUIRED FOR THE END-TO-ENDTESTS 1 BUILD & PACKAGE Package the app for production-like deployment 3 0 VIRTUAL MACHINE Fire-up a local VM instance with production-like env RUN TESTS Execute end-to-end tests against the VM 2 CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box

Slide 59

Slide 59 text

DEPLOYMENT THE SCRIPTS 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) (Provision in Vagrant parlance)

Slide 60

Slide 60 text

ANSIBLE BUT ANY CM TOOL WILL DO Specifically, (Chef, Puppet, Shell scripts)

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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 INVOKES THE PROVISIONER

Slide 64

Slide 64 text

CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box BUILD & PACKAGE Package the app for production-like deployment VIRTUAL MACHINE Fire-up a local VM instance with production-like env STEPS REQUIRED FOR THE END-TO-ENDTESTS 3 RUN TESTS Execute end-to-end tests against the VM 2 1 0

Slide 65

Slide 65 text

CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box BUILD & PACKAGE Package the app for production-like deployment VIRTUAL MACHINE Fire-up a local VM instance with production-like env STEPS REQUIRED FOR THE END-TO-ENDTESTS 3 RUN TESTS Execute end-to-end tests against the VM 2 1 0 CHECK OUT CODE EXAMPLE

Slide 66

Slide 66 text

2 CONFIGURATION & DEPLOYMENT Run production config and deployment code, having our entire system on one box STEPS REQUIRED FOR THE END-TO-ENDTESTS 1 BUILD & PACKAGE Package the app for production-like deployment 0 VIRTUAL MACHINE Fire-up a local VM instance with production-like env 3 RUN TESTS Execute end-to-end tests against the VM

Slide 67

Slide 67 text

WRITING THE Finally, TESTS THEMSELVES

Slide 68

Slide 68 text

GUIDELINES SOME E2E TESTS FOR effective 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

Slide 69

Slide 69 text

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!" } } }

Slide 70

Slide 70 text

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!" } } } USING SPECS2

Slide 71

Slide 71 text

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!" } } }

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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!" } } } TEST IS DECOUPLED FROM THE CONTROLLER API ITSELF BY USING DRIVERS

Slide 74

Slide 74 text

Interact with the SUT directly instead of the test Coupled to the API/protocol Run same test with different drivers Drivers: ABSTRACTING THE SUT

Slide 75

Slide 75 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) } ... }

Slide 76

Slide 76 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

Slide 77

Slide 77 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

Slide 78

Slide 78 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

Slide 79

Slide 79 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 class AddNoteResponse(response: Response) extends BaseResponse(response) with JsonResponse { ! lazy val noteId = (json \ "noteId").extract[String] }

Slide 80

Slide 80 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 class AddNoteResponse(response: Response) extends BaseResponse(response) with JsonResponse { ! lazy val noteId = (json \ "noteId").extract[String] } ONLY THE DRIVER KNOWS THE RESPONSE IS JSON

Slide 81

Slide 81 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 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

Slide 82

Slide 82 text

(back to the test…)

Slide 83

Slide 83 text

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!" } } }

Slide 84

Slide 84 text

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!" } } }

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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!" } } }

Slide 87

Slide 87 text

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!" } } } trait ResponseMatchers extends Matchers { private implicit def intToIntMatcher(t: Int): Matcher[Int] = beEqualTo(t) ! def beOk = haveStatus(200) def beBadRequest = haveStatus(400) def beNotFound = haveStatus(404) ! def haveStatus(status: Matcher[Int]): Matcher[Response] = ((_: Response).status) ^^ status }

Slide 88

Slide 88 text

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!" } } }

Slide 89

Slide 89 text

LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX

Slide 90

Slide 90 text

LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX

Slide 91

Slide 91 text

TESTING EXTERNAL SERVICES

Slide 92

Slide 92 text

As always, LIFE IS ABOUT COMPROMISES AND SETTING BOUNDARIES

Slide 93

Slide 93 text

As always, LIFE IS ABOUT COMPROMISES AND SETTING BOUNDARIES => USE FAKES

Slide 94

Slide 94 text

FAKES FOR 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 USE EXTERNAL SERVICES NOT EASILY AVAILABLE(Sometimes called Simplicators)

Slide 95

Slide 95 text

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() } }

Slide 96

Slide 96 text

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 FakeYandexTranslateServer(port: Int) extends SimpleHttpServer(port) { ! private val json = """{ |"code": 200, |"lang": "en-es", |"text": ["Buenos días"] |} """.stripMargin ! override protected def onSimpleRequest(request: HttpRequest): String = json }

Slide 97

Slide 97 text

LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX

Slide 98

Slide 98 text

LOAD BALANCER HAPROXY REVERSE PROXY NGINX APP SERVER FINATRA DATASTORE ELASTICSEARCH TRANSLATION SERVICE YANDEX

Slide 99

Slide 99 text

WHAT YOU SHOULD DO NEXT

Slide 100

Slide 100 text

CONTINUE FROM HERE How to 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

Slide 101

Slide 101 text

CONTINUE FROM HERE How to 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

Slide 102

Slide 102 text

THANK YOU

Slide 103

Slide 103 text

No content

Slide 104

Slide 104 text

No content