Unit Testing
In any program which is bigger than arithmetic operations, programmers should make themselves comfortable when it is possible to ensure that new changes are not breaking old functionalities.
The most common technique for this is unit testing, which is where the programmer tests the functionality of the code in parallel with its development by creating a test code which will verify that the code really satisfies their requirements.
The theme of this section will be introducing tools for unit testing in Scala.
Adding a Test to Our Project
Let's add tests
to our small program. We'll import
<for-students/lesson1/2-project>
in our IDE.
This is the directory schema of a Scala project. For adding tests, we should do the following:
- Add test dependencies to
build.sbt
- Write tests in the source test directory
For adding dependency, let's add the following line to our
build.sbt
:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.4" % "test"
It's an
expression in Scala DSL (domain-specific language), which means that we should add
scalatest
to our set of library
dependencies. Operators
%%
and
%
are used for forming the name and classifier for published artifacts. You can refer to the
sb
t
documentation for more detail:
http://www.scala-sbt.org/1.x/docs/Library-Dependencies.html.
Before compilation,
sbt
will download
scalatest
from a publicly available repository (Maven central), and when running tests, it will add
scalatest
to the classpath.
We will
now run
sbt
tests from the command line.
- In the command-line environment, navigate to the root of the project and select the following test:
Lesson 1/2-project
- If you are using a Unix/Linux machine and your code is situated in
courses/pactscala
of your homedirectory
, then run the following command:> cd ~/courses/packscala/Lesson 1/2-project
- Run the following command
:
> sbt test
- You will get the expected output, which will include the following strings:
[info] ExampleSpec: [info] - example test should pass [info] StepTest: [info] - step of unparded word must be interesting
We will now see how to
run
sbt
tests from IDEA IDE.
We'll now run sbt Tests from IDEA IDE.
- Open the project in the IDE.
- Navigate to Run/Edit Configurations:
- Choose s
bt test
as the configuration.- Check the checkbox Use sbt:
- Select Run sbt-test.
Inside Tests
Now let's look at a simple test:
package com.packt.courseware.l1 import org.scalatest.FunSuite class ExampleSpec extends FunSuite { test("example test should pass") { assert(1==1) } }
Here, we define a class which is inherited from scalatest FunSuite.
The test expression is called. When the
FunSuite
class is initialized and added to a set of tests, the test with
name example test should pass
and assert an expression as an argument. For now, this looks like magic, but we will show you how to build such DSLs in the next chapter.
Let's run our test with the help of
sbt
:
sbt test
This command will run all tests and evaluate the test expression.
Now, we'll add another test.
- Add one more test to the same file:
src/test/scala/com/packt/courseware/l1/ExampleSpec.scala in 2-project
- We write one
trivial
test, which asserts thefalse
expression:test("trivial") { assert(false) }
- Run the test and look at error reporting.
- Invert the expression in assert so that the test passes:
test("trivial") { assert(true) }
- Run the
sbt
test again to ensure that all of the tests pass.
Running Tests for Chatbot
Remember that,
when writing
chatbot
, we want to test one functionality. Our original program only has one function (
main
), which contains all of the logic and can't be split into testable parts.
Let's look at Version 2.
Note
Please import
Lesson 1/2-project
into your IDE.
package com.packt.courseware.l1 import java.time.LocalTime import java.time.format.DateTimeFormatter import scala.io.StdIn case class LineProcessResult(answer:String,timeToBye:Boolean) object Chatbot2 { def main(args: Array[String]): Unit = { val name = StdIn.readLine("Hi! What is your name? ") println(s" $name, tell me something interesting, say 'bye' to end the talk") var c = LineProcessResult("",false) while(!c.timeToBye){ c = step(StdIn.readLine(">")) println(c.answer) } } def step(input:String): LineProcessResult = { input match { case "bye" => LineProcessResult("ok, bye", true) case "time" => LineProcessResult(LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")),false) case _ => LineProcessResult("interesting...", false) } } }
Here, we see some new constructs:
LineProcessingResult
is a case class, where the result of processing one of the lines (that is, the
chatbot
answer and quit flag) is stored.
What is the word
case
before class?
case
classes can
participate in pattern matching (while we call one
case
) and are usually used for data objects. We will look at
case
classes during the next chapter. It is important to see that an instance of
case
classes can be created with the
LineProcessingResult(x,y)
syntax (that is, without
new
) and an argument to case class constructors (
answers
and
timeToBye
), which automatically become instance variables of the
case
class.
The functionality of processing one line is encapsulated in the
step
method, which we can test.
Step
receives input from the method argument, not from
System.in
, therefore making it easier to test. In the case of directly testing the
main
method, we will need to substitute
System.in
before
test
and return one back after the test is finished.
Ok, let's focus on the first test:
package com.packt.courseware.l1 import org.scalatest.FunSuite class StepTestSpec extends FunSuite { test("step of unparded word must be interesting") { val r = Chatbot2.step("qqqq") assert(! r.timeToBye) assert(r.answer == "interesting...") } }
Writing the second test in the same manner will be an easy task. We will look at this in the following exercise.
Now, let's add the second test, which checks bye.
- Add a second test to the
StepTestSpec
class in our project:test("after bye, timeToBye should be set to true") { }
- In this test:
- Call the step function with
bye
as a parameter:val r = Chatbot2.step("bye")
- Check that after this call that
timeToQuit
in the returned class is set totrue
:assert(! r.timeToBye)
- Call the step function with
- The whole code should be as follows:
test("after bye, timeToBye should be set to true") { val r = Chatbot2.step("bye")
assert(! r.timeToBye) - Run
sbt test.
A more complex task would be to write a test for the time query.
Please note that we can't run the test with the concrete time value, but at least we can be sure that the bot answer can't be parsed back to the time form.
So, what can we do to check the line answer and try to transform it back to time? The solution is provided in the following code:
test("local time must be parser") { val r = Chatbot2.step("time") val formatter = DateTimeFormatter.ofPattern("HH:mm:ss") val t = LocalTime.parse(r.answer,formatter)// assertion is not necessary }
Note that assertion is not necessary. If time does not satisfy the given format, then an exception will be thrown.
It is a good practice to separate functional and effects time for testing. To do this, we will need to substitute the provider of the system time via own.
This will be the first practical task in the next chapter.
Now, let's add the date command to our chatbot program.
- Add the following code to the match statement so that it checks for the
date
command, which should output the local date inDD:MM:YYYY
format:case "date" => LineProcessResult(LocalDate.now().format(DateTimeFormatter.ofPattern("dd:YYYY-MM")),false)
- Add a test case for this function.
- The resulting code will be as follows:
test("local date must be parser") { val r = Chatbot2.step("date") val formatter = DateTimeFormatter.ofPattern("dd:MM-YYYY") val t = LocalDate.parse(r.answer,formatter)// assertion is not necessary }