Book Image

Professional Scala

By : Mads Hartmann, Ruslan Shevchenko
Book Image

Professional Scala

By: Mads Hartmann, Ruslan Shevchenko

Overview of this book

This book teaches you how to build and contribute to Scala programs, recognizing common patterns and techniques used with the language. You’ll learn how to write concise, functional code with Scala. After an introduction to core concepts, syntax, and writing example applications with scalac, you’ll learn about the Scala Collections API and how the language handles type safety via static types out-of-the-box. You’ll then learn about advanced functional programming patterns, and how you can write your own Domain Specific Languages (DSLs). By the end of the book, you’ll be equipped with the skills you need to successfully build smart, efficient applications in Scala that can be compiled to the JVM.
Table of Contents (12 chapters)

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.

  1. In the command-line environment, navigate to the root of the project and select the following test:

      Lesson 1/2-project
  2. If you are using a Unix/Linux machine and your code is situated in courses/pactscala of your home directory, then run the following command:

     > cd  ~/courses/packscala/Lesson 1/2-project
  3. Run the following command :

        > sbt test
  4. 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.

  1. Open the project in the IDE.

  2. Navigate to Run/ Edit Configurations:

  3. Choose s bt test as the configuration.

    • Check the checkbox Use sbt:

  4. 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.

  1. Add one more test to the same file: src/test/scala/com/packt/courseware/l1/ExampleSpec.scala in 2-project

  2. We write one trivial test, which asserts the false expression:

          test("trivial")  {
                assert(false)
           }
  3. Run the test and look at error reporting.

  4. Invert the expression in assert so that the test passes:

          test("trivial")  {
                assert(true)
           }
  5. 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.

  1. Add a second test to the StepTestSpec class in our project:

    test("after bye, timeToBye should be set to true")
    {
    
    }
  2. 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 to true:

      assert(! r.timeToBye)
  3. The whole code should be as follows:

    test("after bye, timeToBye should be set to true") {  
    val r = Chatbot2.step("bye")
    assert(! r.timeToBye)
    
  4. 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.

  1. Add the following code to the match statement so that it checks for the date command, which should output the local date in DD:MM:YYYY format:

      case "date" => LineProcessResult(LocalDate.now().format(DateTimeFormatter.ofPattern("dd:YYYY-MM")),false)
  2. Add a test case for this function.

  3. 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
    }