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
    }