Scala - JUnit Testing



JUnit is a testing framework for Java applications. It is used to write and run repeatable automated tests for correctness of your code. You can use JUnit for testing Scala applications. There are various features and integration capabilities within Scala projects.

You can write unit tests and integration tests using JUnit. You can also test performance to validate the behavior and performance of your application.

JUnit 4 with Scala

Dependencies

You need to add the following dependency to your build.sbt file to use JUnit 4 in a Scala project -

libraryDependencies += "com.github.sbt" % "junit-interface" % "0.13.3" % "test"

Now, you can run JUnit tests from SBT.

Writing and Running Tests

Consider this example for a simple JUnit 4 test in Scala -

package com.example.scala

import org.junit.Test
import org.junit.Assert._

class IntJunitTests {

  @Test
  def testOneIsPositive(): Unit = {
    assertTrue(1 > 0)
  }

  @Test
  def testMinusOneIsNegative(): Unit = {
    assertTrue(-1 < 0)
  }
}

You can use the test command to run the tests from the sbt shell -

sbt test

The output will be,

[info] Passed: Total 2, Failed 0, Errors 0, Passed 2
[success] Total time: 3 s, completed 08-Aug-2024, 11:45:48 am   

Advanced Test Execution

You can also run specific tests using commands like testOnly -

sbt "testOnly com.example.scala.IntJunitTests"

The output will be,

[info] Passed: Total 2, Failed 0, Errors 0, Passed 2
[success] Total time: 0 s, completed 08-Aug-2024, 11:46:47 am       

You can run a single test within a class -

sbt "testOnly -- *.IntJunitTests.testOneIsPositive"

The output will be,

[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 0 s, completed 08-Aug-2024, 11:47:29 am       

Test Suites

JUnit also supports test suites. You can group multiple test classes. For example,

package com.example.scala

import org.junit.runner.RunWith
import org.junit.runners.Suite

@RunWith(classOf[Suite])
@Suite.SuiteClasses(Array(classOf[IntJunitTests], classOf[StringJunitTests]))
class TypesTestSuite

You can run the test suite -

sbt "testOnly *.TypesTestSuite"

Custom Test Runner

Sometimes, you may need to customize the test runner to handle specific test execution requirements. You can create custom test runner, like this -

import org.junit.runner.RunWith
import org.scalatestplus.junit.JUnitRunner

@RunWith(classOf[JUnitRunner])
class CustomTestRunner extends JUnitSuite {
  // Custom test logic here
}

JUnit 5 with Scala

Dependencies and Setup

JUnit 5 requires a different setup compared to JUnit 4. You need to add the following to your plugins.sbt -

resolvers += Resolver.jcenterRepo
addSbtPlugin("net.aichler" % "sbt-jupiter-interface" % "0.8.3")

Now, you should have this following dependency in your build.sbt -

libraryDependencies += "net.aichler" % "jupiter-interface" % "0.8.3" % Test

Writing JUnit 5 Tests

Consider this example of using JUnit 5 in Scala -

package com.example.scala

import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test

class ExampleTests {

  @Disabled("Not implemented yet")
  @Test
  def testNotImplemented(): Unit = {}
}

Now, you can run the tests using -

sbt test

The output will be,

[success] Total time: 3 s, completed 08-Aug-2024, 11:52:25 am 

Test Lifecycle Management

JUnit 5 provides annotations for managing the lifecycle of tests, like, @BeforeAll, @AfterAll, @BeforeEach, and @AfterEach. Here is how you can use these in Scala -

package com.example.scala

import org.junit.jupiter.api.{BeforeAll, AfterAll, BeforeEach, AfterEach, TestInstance, Test}
import org.junit.jupiter.api.TestInstance.Lifecycle

@TestInstance(Lifecycle.PER_CLASS)
class LifecycleTests {

  @BeforeAll
  def beforeAll(): Unit = {
    println("Before all tests")
  }

  @AfterAll
  def afterAll(): Unit = {
    println("After all tests")
  }

  @BeforeEach
  def beforeEach(): Unit = {
    println("Before each test")
  }

  @AfterEach
  def afterEach(): Unit = {
    println("After each test")
  }

  @Test
  def testExample(): Unit = {
    println("Executing test")
  }
}

Note that you should have these dependencies in your build.sbt file -

scalaVersion := "2.13.14"

libraryDependencies ++= Seq(
  "org.junit.jupiter" % "junit-jupiter-api" % "5.8.1" % "test",
  "org.junit.jupiter" % "junit-jupiter-engine" % "5.8.1" % "test",
  "org.junit.platform" % "junit-platform-launcher" % "1.8.1" % "test"
)

testFrameworks += new TestFramework("munit.Framework")

Now, you can compile and run test using these commands -

sbt clean compile
sbt run

The output will be,

[success] Total time: 3 s, completed 08-Aug-2024, 12:00:04 pm 

ScalaTest Integration

Using ScalaTest with JUnit

ScalaTest provides more readable and concise assertions. You can use ScalaTest assertions with JUnit, mix in AssertionsForJUnit. For example,

package com.example.scala

import org.scalatestplus.junit.AssertionsForJUnit
import org.junit.Assert._
import org.junit.Test

class ExampleSuite extends AssertionsForJUnit {

  @Test def verifyEasy(): Unit = {
    assertEquals("ScalaTest is easy!", "ScalaTest is easy!")
  }

  @Test def verifyFun(): Unit = {
    assert("ScalaTest is fun!" == "ScalaTest is fun!")
  }
}

You need to add the following dependencies to your build.sbt -

import Dependencies._

ThisBuild / scalaVersion := "2.13.14"

lazy val root = (project in file("."))
  .settings(
    name := "Demo",
    version := "0.1",
    libraryDependencies ++= Seq(
      "org.scalatest" %% "scalatest" % "3.2.12" % "test",
      "com.github.sbt" % "junit-interface" % "0.13.3" % "test",
      "org.scalatestplus" %% "junit-4-12" % "3.2.2.0" % "test"
    )
  )

The output will be,

[info] Passed: Total 2, Failed 0, Errors 0, Passed 2
[success] Total time: 1 s, completed 08-Aug-2024, 2:14:31 pm 

Combining ScalaTest and JUnit

You can write tests that can be run by both JUnit and ScalaTest. For example,

import org.scalatestplus.junit.JUnitSuite
import org.junit.Assert._
import org.junit.Test
import org.junit.Before

class CombinedSuite extends JUnitSuite {

  var sb: StringBuilder = _
  var lb: ListBuffer[String] = _

  @Before def initialize(): Unit = {
    sb = new StringBuilder("ScalaTest is ")
    lb = new ListBuffer[String]
  }

  @Test def verifyEasy(): Unit = {
    sb.append("easy!")
    assertEquals("ScalaTest is easy!", sb.toString)
    assertTrue(lb.isEmpty)
    lb += "sweet"
    try {
      "verbose".charAt(-1)
      fail()
    } catch {
      case e: StringIndexOutOfBoundsException => // Expected
    }
  }

  @Test def verifyFun(): Unit = {
    sb.append("fun!")
    assert(sb.toString === "ScalaTest is fun!")
    assert(lb.isEmpty)
    lb += "sweeter"
    intercept[StringIndexOutOfBoundsException] {
      "concise".charAt(-1)
    }
  }
}

So, there can be flexibility in using either test framework.

Example: Testing a Calculator

Consider this test a simple calculator application -

package com.example.scala

import org.junit.Test
import org.junit.Assert._

class CalculatorTests {

  class Calculator {
    def add(a: Int, b: Int): Int = a + b
    def subtract(a: Int, b: Int): Int = a - b
    def multiply(a: Int, b: Int): Int = a * b
    def divide(a: Int, b: Int): Int = if (b != 0) a / b else throw new ArithmeticException("Division by zero")
  }

  val calculator = new Calculator

  @Test
  def testAddition(): Unit = {
    assertEquals(5, calculator.add(2, 3))
  }

  @Test
  def testSubtraction(): Unit = {
    assertEquals(1, calculator.subtract(3, 2))
  }

  @Test
  def testMultiplication(): Unit = {
    assertEquals(6, calculator.multiply(2, 3))
  }

  @Test(expected = classOf[ArithmeticException])
  def testDivisionByZero(): Unit = {
    calculator.divide(1, 0)
  }

  @Test
  def testDivision(): Unit = {
    assertEquals(2, calculator.divide(6, 3))
  }
}

Example: Testing a Simple Scala Class

Consider this example of testing a simple Scala class -

package com.example.scala

import org.junit.Test
import org.junit.Assert._

class Person(val name: String, val age: Int)

class PersonTests {

  @Test
  def testPersonCreation(): Unit = {
    val person = new Person("John Doe", 30)
    assertEquals("John Doe", person.name)
    assertEquals(30, person.age)
  }

  @Test
  def testPersonAge(): Unit = {
    val person = new Person("Jane Doe", 25)
    assertTrue(person.age > 20)
  }
}
Advertisements