Scala/Mill: Step 2, Using ScalaTest with Mill

In this next step we’ll add ScalaTest and some unit tests to our Mill project to see how that works.

Mill’s directory structure

Because Mill’s directories are different than other build tools like Maven and SBT, the first thing to do is to create a directory structure for our tests. The way to do this is to create a test directory under the HelloWorld directory, at the same level as the src directory. To create the entire structure we need with one command, including a directory for our hello package, use this command:

mkdir -p HelloWorld/test/src/test/hello

I’ll also create the test file in that directory to be clear about what I’m doing:

touch HelloWorld/test/src/test/hello/HelloTests.scala

In a few moments I’ll add some tests to that HelloTests.scala file.

Here’s what the directory structure looks like now:

$ tree HelloWorld

HelloWorld
├── src
│   └── main
│       ├── resources
│       └── scala
│           └── hello
│               └── Hello.scala
└── test
    └── src
        └── test
            └── hello
                └── HelloTests.scala

To reiterate the point, the test directory and the src directory are at the same level in the hierarchy, both directly underneath the HelloWorld directory.

Updating MIll’s build.sc file

With that test directory structure in place, the next thing to do is to update our build.sc file to (a) add the ScalaTest dependencies and also (b) reflect that test directory. Here’s the updated file:

import mill._, scalalib._

object HelloWorld extends ScalaModule {
    def scalaVersion = "2.12.11"

    object test extends Tests {
        def ivyDeps = Agg(
            ivy"org.scalactic::scalactic:3.1.1",
            ivy"org.scalatest::scalatest:3.1.1"
        )
        def testFrameworks = Seq("org.scalatest.tools.Framework")
    }
}

Notice a few things:

  • I create the test object inside the HelloWorld object
  • Dependencies are added with the ivyDeps parameter
  • Mill uses an ivy string interpolator (similar to other interpolators like s, raw, f) and the syntax shown for Scala resources
  • I haven’t looked into the Agg class yet, but it looks like a Seq, so that’s good enough for now

A thing I appreciate at this point is that the build.sc file is just plain Scala code. I’m not a huge fan of most DSLs, so that’s a nice win.

Writing the tests

To create the tests, first edit the Hello.scala file so we have something to test. Here are the updated contents of that file:

package hello

object Hello extends App {
    println(Constants.hello)
}

object Constants {
    val hello = "Hello, world"
}

Now lets edit our ScalaTest test file. As a reminder, this is its name and path:

HelloWorld/test/src/test/hello/HelloTests.scala

This is a normal ScalaTest file, so put these contents in that file:

package hello
  
import org.scalatest.funsuite.AnyFunSuite

class HelloSuite extends AnyFunSuite {

    test("Test that ‘Hello’ string is correct") {
        assert(Constants.hello == "Hello, world")
    }

    test ("Another test ...") (pending)

}

Running the tests

To run the tests, use this Mill command:

mill HelloWorld.test

Its output looks like this:

Compiling MillTest1/build.sc
[50/56] HelloWorld.test.compile 
[info] Compiling 1 Scala source to MillTest1/out/HelloWorld/test/compile/dest/classes ...
[info] Done compiling.
[56/56] HelloWorld.test.test 
HelloSuite:
- Test that ‘Hello’ string is correct
- Another test ... (pending)

Cool, the tests work.

If you want to continually run the tests while you’re working, use Mill’s “watch” option:

$ mill --watch HelloWorld.test

[56/56] HelloWorld.test.test 
HelloSuite:
- Test that ‘Hello’ string is correct
- Another test ... (pending)
Watching for changes to 2 dirs and 4 files... (Ctrl-C to exit)

Cool, ScalaTest seems to work pretty easily with Mill.