title | date | fontsize | monofont | mainfont | header-includes | ||
---|---|---|---|---|---|---|---|
TDD tutorial |
\today |
11pt |
Menlo |
Avenir |
|
A TDD tutorial based on ReactiveMongo and particularly the PullRequest #750.
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)
Dev tools:
- Git client
- JDK 1.8+
- SBT
Knowledge:
- required: Scala 2.12+
- optional: specs2 (test framework)
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.
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).
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)
You can paste the following snippet to test SRV resolution.
import org.xbill.DNS._
new Lookup("_imaps._tcp.gmail.com", Type.SRV).run()
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)
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)
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()
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,
- the "not implemented" function
srvRecords
, - the corresponding 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
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)
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
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
.
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)
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")
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
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).
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)
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
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
.
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)
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)
See the refactoring online
According this refactoring, the tests in UtilSpec.scala
need to be updated.
Time to update tests: 1min (solution thereafter)
See the updated tests
Once done it can be used from SBT to make sure no regression is introduced:
testOnly UtilSpec
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
.
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)
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)
See the refactoring online
According this refactoring, the tests in UtilSpec.scala
need to be updated.
Time to update tests: 1min (solution thereafter)
See the updated tests
Once done it can be used from SBT to make sure no regression is introduced:
testOnly UtilSpec
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
.
You can refactor both srvRecords
and txtRecords
,
to return Future
(for composition & error handling).
Time to code: 15min (solution thereafter)
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)
See the updated tests
Once done it can be used from SBT to make sure no regression is introduced:
testOnly UtilSpec
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
.
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)
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)
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
.
You can have a look at a complete solution for all this tutorial in the solution
branch.