cchantep / tdd-tutorial

A TDD tutorial based on ReactiveMongo and particularly the PullRequest reactivemongo/reactivemongo#750

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

title date fontsize monofont mainfont header-includes
TDD tutorial
\today
11pt
Menlo
Avenir
\usepackage{pandoc-solarized}
\input{beamer-includes}

TDD tutorial

A TDD tutorial based on ReactiveMongo and particularly the PullRequest #750.

Introduction

The goal is to share a TDD example, based on ReactiveMongo code.

The Pull Request #750, which aims to implement a support for DNS seed list, will be used as subject.

Estimated time: 1h 30min (reading time 10min)

Keywords: tdd, scala, specs2

Requirements

Dev tools:

Knowledge:

Start the tutorial

You first need to git-clone this tutorial:

git clone git@github.com:cchantep/tdd-tutorial.git \
  --branch master --single-branch

Then the TDD approach is described step by step thereafter.

Phase 1 - Setup dnsjava

We will be using the library dnsjava, so at first step you need to add the corresponding dependency (version 2.1.8) in the build.sbt (make sure to reload the build in SBT).

Once that's ok, we will starting testing right now, by checking how to use dnsjava to resolve SRV records.

At this point you can start the REPL using sbt console (or just type console if SBT is already launched).

Phase 1 - Read documentation & first test

When the SBT Scala REPL is started, it's time to read the documentation to figure out how to perform a SRV lookup, and test it for _imaps._tcp.gmail.com in the REPL.

It should print an expected result as below:

Array[org.xbill.DNS.Record] = Array(
  _imaps._tcp.gmail.com.	21599	IN	SRV	\
  5 0 993 imap.gmail.com.)

Time to code & test: 10min (solution thereafter)

Phase 1 - SRV Lookup in the REPL

You can paste the following snippet to test SRV resolution.

import org.xbill.DNS._

new Lookup("_imaps._tcp.gmail.com", Type.SRV).run()

Phase 1 - Refactor as a function

Still in the REPL, you can now implement the following function using the tested lookup code, to return only the target name of the resolved SRV record.

import org.xbill.DNS.Name
def records(name: String): Array[Name] = ???
// lookup for SRV record _imaps._tcp.<name>

This function can immediately by tested in the REPL.

records("gmail.com")
// Expected result: ... Array(imap.gmail.com.)

Time to code & test: 5min (solution thereafter)

Phase 1 - REPL records function

The records function can be declared as below in the REPL.

def records(name: String): Array[Name] =
  new Lookup(s"_imaps._tcp.${name}", Type.SRV).
  run().map(_.getAdditionalName)

Phase 1 - Timeout resolution

In order to configure the timeout for the SRV resolution, a custom Resolver can be used as follows (you can test it in the REPL).

import org.xbill.DNS._

def customResolver: Resolver = {
  val r = Lookup.getDefaultResolver
  r.setTimeout(5/*seconds*/)
  r
}

val lkup = new Lookup("_imaps._tcp.gmail.com", Type.SRV)
lkup.setResolver(customResolver)
lkup.run()

Phase 1 - Contracts as signature & tests

After these interactive tests in the REPL, according the TDD approach we will go on with the TDD approach, first writing a test for the SRV resolution.

The expected contracts for such resolution are written as,

Phase 1 - Start with failing test

This test can be executed in SBT:

testOnly -- include srvRecords

At this point, without srvRecords implementation, the test is expected to fail.

[info] Utilities
[error]   x resolve SRV record for _imaps._tcp at ..
[error]    an implementation is missing (..)
[info] Total for specification Utilities
[info] Finished in 185 ms
[info] 1 example, 1 failure, 0 error

Phase 1 - Implement srvRecords to match acceptance

Now it's time to make the test happy, by implementing srvRecords so it matches its contract.

To do so, you can edit the function in UtilSpec.

The SBT task must be executed continuously until the function no longer raises any error.

testOnly UtilSpec -- include srvRecords

Time to code & test: 5min (solution thereafter)

Phase 1 - Solution & expected result

See the online solution

Expected test results:

[info] Utilities
[info]   + resolve SRV record for _imaps._tcp at ...
[info] Total for specification Utilities
[info] Finished in 195 ms
[info] 1 example, 0 failure, 0 error
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1

Phase 1 - Commit

At this step, it's time to commit the changes applied on build.sbt and UtilSpec.scala.

git commit -a -m "Phase #1"

The code expected after this phase can be checked with the online tag tdd/phase1.

Phase 2 - List TXT records

We know how to resolve SRV records, to implements DNS seed list, TXT records are also required.

You can first test it for domain gmail.com in the REPL: sbt console

Considering only the RDATA for each record, the expected result is Array("v=spf1 redirect=_spf.google.com")

Time to code & test: 10min (solution thereafter)

Phase 2 - TXT lookup in REPL

The TXT records for gmail.com can be checked as below.

import org.xbill.DNS._

new Lookup("gmail.com", Type.TXT).
  run().map(_.rdataToString)
// => Array("v=spf1 redirect=_spf.google.com")

Phase 2 - Contracts as signature & tests

After these REPL tests, we will save it in UtilSpec.scala with the "not implemented" function txtRecords.

According the function signature, you have to write the test in UtilSpec to check that TXT resolution for gmail.com returns v=spf1 redirect=_spf.google.com.

"resolve TXT record for gmail.com" in {
  todo /* List("v=spf1 redirect=_spf.google.com") */
}

Time to code test: 1min

Phase 2 - Start with failing test

As soon as the test for txtRecords is written, it can be used in SBT.

testOnly

For now, this test is expected to fail (without the function implementation).

Phase 2 - Implement txtRecords to match acceptance

From there, you can implement the txtRecords function in UtilSpec.

Until the corresponding test is successful, the SBT task can be executed continuously.

testOnly UtilSpec

Time to code & test: 5min (solution thereafter)

Phase 2 - Solution & expected result

See the online solution

Expected test results:

[info] Utilities
...
[info] DNS resolver should
[info]   + resolve SRV record for _imaps._tcp at ...
[info]   + resolve TXT record for gmail.com
[info] Total for specification Utilities
[info] Finished in 331 ms
[info] 4 examples, 0 failure, 0 error
[info] Passed: Total 4, Failed 0, Errors 0, Passed 4

Phase 2 - Commit

At this step, it's time to commit the changes applied on UtilSpec.scala.

git commit -a -m "Phase #2"

The code expected after this phase can be checked with the online tag tdd/phase2.

Phase 3 - Refactor srvRecords function

At this step, the function srvRecords can be moved in the package object reactivemongo.util.

While moving this function, you should also take the opportunity to add it the following parameter.

timeout: FiniteDuration

Time to code: 5min (solution thereafter)

Phase 3 - Document srvRecords function

In order to finalize the srvRecords function, you can document it, with a Scaladoc as below.

/**
 * @param name the DNS name (e.g. `my.mongodb.com`)
 * @param timeout the resolution timeout (default: 5s)
 * @param srvPrefix the SRV prefix (e.g. `_mongodb._tcp`)
 */

Time to document: 1min (solution thereafter)

Phase 3 - Refactored svrRecords & Scaladoc

See the refactoring online

According this refactoring, the tests in UtilSpec.scala need to be updated.

Time to update tests: 1min (solution thereafter)

Phase 3 - Test refactoring of srvRecords

See the updated tests

Once done it can be used from SBT to make sure no regression is introduced:

testOnly UtilSpec

Phase 3 - Commit

At this step, it's time to commit the changes applied on package.scala and UtilSpec.scala.

git commit -a -m "Phase #3"

The code expected after this phase can be checked with the online tag tdd/phase3.

Phase 4 - Refactor txtRecords function

As for srvRecords, the function txtRecords can also be moved in the package object reactivemongo.util.

While moving this function, you should also take the opportunity to add it the following parameter.

timeout: FiniteDuration

Time to code: 5min (solution thereafter)

Phase 4 - Document srvRecords function

In order to finalize the srvRecords function, you can document it, with a Scaladoc as follows.

/**
 * @param name the DNS name (e.g. `my.mongodb.com`)
 * @param timeout the resolution timeout (default: 5s)
 */

Time to document: 1min (solution thereafter)

Phase 4 - Refactored txtRecords & Scaladoc

See the refactoring online

According this refactoring, the tests in UtilSpec.scala need to be updated.

Time to update tests: 1min (solution thereafter)

Phase 4 - Test the refactoring

See the updated tests

Once done it can be used from SBT to make sure no regression is introduced:

testOnly UtilSpec

Phase 4 - Commit

At this step, it's time to commit the changes applied on package.scala and UtilSpec.scala.

git commit -a -m "Phase #4"

The code expected after this phase can be checked with the online tag tdd/phase4.

Phase 5 - Refactor srvRecords & txtRecords

You can refactor both srvRecords and txtRecords, to return Future (for composition & error handling).

Time to code: 15min (solution thereafter)

Phase 5 - Refactoring & Scaladoc

See the refactoring online

According this refactoring, the tests in UtilSpec.scala need to be updated.

(See specs2 documentation about testing Future)

Time to update tests: 5min (solution thereafter)

Phase 5 - Test the refactoring

See the updated tests

Once done it can be used from SBT to make sure no regression is introduced:

testOnly UtilSpec

Phase 5 - Commit

At this step, it's time to commit the changes applied on package.scala and UtilSpec.scala.

git commit -a -m "Phase #5"

The code expected after this phase can be checked with the online tag tdd/phase5.

Phase 6 - Refactor txtRecords with ListSet

As the TXT resolution must only consider the distinct records, the function txtRecords can be refactored to use ListSet rather than List.

Time to code: 5min (solution thereafter)

Phase 6 - Refactored txtRecords

See the online solution

After this refactoring, the tests must be failing when using testOnly in SBT. So UtilSpec.scala need to be updated accordingly.

Time to update tests: 1min (solution thereafter)

Phase 6 - Tests & commit

See the updated tests

At this step, it's time to commit the changes applied on package.scala and UtilSpec.scala.

git commit -a -m "Phase #6"

The code expected after this phase can be checked with the online tag tdd/phase6.

Complete solution

You can have a look at a complete solution for all this tutorial in the solution branch.

About

A TDD tutorial based on ReactiveMongo and particularly the PullRequest reactivemongo/reactivemongo#750

License:Apache License 2.0


Languages

Language:Scala 58.9%Language:TeX 41.1%