CTCaliberTek
Course: Scala 3 Developer Path: From First Program to a Tested Project · Lesson 8 of 8Log in to track your progress and earn a certificate when you finish the course.
Programming

Build and Test a Scala Project with sbt and munit

Published November 9, 2026·Updated November 9, 2026·10 min read

Edited by , Senior Data Analyst. How we make our content

Like SaveLog in to like, save, and track progress.

Introduction

Single files are fine for learning, but real Scala work happens in projects with several files, dependencies, and tests. sbt, the Scala Build Tool, compiles code, downloads libraries, runs tests, and packages applications. This lesson creates a small project, explains the files sbt expects, and writes unit tests with munit, a lightweight test library, so you can build the habit of testing from the start.

Prerequisites

  • Scala and sbt installed through Coursier (first lesson)
  • The language fundamentals from the previous lessons

Step-by-Step Guide

Step 1: Generate a project from a template

The official Scala 3 template creates a ready-to-build project with sbt's new command. It asks for a project name and writes the files into a new folder. Look at what it creates: a build.sbt file at the root, a project folder holding build tooling settings, and a src folder with main and test subfolders. This standard layout is the same in every sbt project, so any Scala developer can find their way around yours.

sbt new scala/scala3.g8
cd my-project
# my-project/
#   build.sbt
#   project/build.properties
#   src/main/scala/Main.scala
#   src/test/scala/...

Step 2: Read build.sbt

The build.sbt file describes the project in Scala syntax. It sets the Scala version, the project name, and the libraries it depends on. A dependency has the form of an organization, a name, and a version, joined by percent signs. A double percent sign appends the Scala version to the library name so you get the build that matches your compiler. The Test marker after a dependency means it is only available to test code. Use the Scala version your template generates, or check scala-lang.org for the current long-term support release.

ThisBuild / scalaVersion := "3.3.4"

lazy val root = (project in file("."))
  .settings(
    name := "calibertek-demo",
    libraryDependencies += "org.scalameta" %% "munit" % "1.0.0" % Test
  )

Step 3: Write some code worth testing

Put application code under src/main/scala. A good first target is pure logic with no input or output, since it needs no setup to test. Here is a small module for a shopping cart total that uses a case class and a method with a rule. Notice it avoids reading from the console or printing: functions that take values and return values are the easiest to test. Keep the side effects, such as printing, in a thin main function that calls this logic.

// src/main/scala/Cart.scala
case class Item(name: String, price: Double, qty: Int)

object Cart:
  def total(items: List[Item], discountPercent: Double = 0): Double =
    require(discountPercent >= 0 && discountPercent <= 100, "discount must be 0-100")
    val subtotal = items.map(i => i.price * i.qty).sum
    subtotal * (1 - discountPercent / 100)

@main def run(): Unit =
  val items = List(Item("pen", 2.5, 4), Item("book", 12.0, 1))
  println(Cart.total(items, discountPercent = 10))   // 19.8

Step 4: Write tests with munit

Tests live under src/test/scala. With munit, a test is a class that extends FunSuite and contains named test blocks. The assertEquals method compares an expected and an actual value and prints a clear diff when they differ. Because floating point numbers can differ in the last digits, compare doubles with a tolerance using the three-argument form of assertEqualsDouble. To check that bad input is rejected, use intercept, which passes only if the block throws the given exception type.

// src/test/scala/CartSuite.scala
class CartSuite extends munit.FunSuite:

  test("total of an empty cart is zero") {
    assertEquals(Cart.total(Nil), 0.0)
  }

  test("total multiplies price by quantity") {
    val items = List(Item("pen", 2.5, 4), Item("book", 12.0, 1))
    assertEqualsDouble(Cart.total(items), 22.0, 0.001)
  }

  test("a discount reduces the total") {
    val items = List(Item("book", 20.0, 1))
    assertEqualsDouble(Cart.total(items, 25), 15.0, 0.001)
  }

  test("an invalid discount is rejected") {
    intercept[IllegalArgumentException] {
      Cart.total(Nil, discountPercent = 150)
    }
  }

Step 5: Run sbt commands

Start sbt from the project root with the sbt command to open its interactive shell, which keeps the compiler warm so later commands are much faster than launching sbt each time. The compile command builds, test runs the suites, and run launches the main function. Prefixing a command with a tilde makes sbt watch the files and rerun it every time you save, which gives a fast feedback loop while you write code. The console command opens a REPL with your project's classes already on the classpath.

sbt compile
sbt test
sbt run
sbt "testOnly CartSuite"

# interactive shell:
sbt
> ~test          # rerun tests on every save
> console        # REPL with your code loaded

Step 6: Add libraries and keep the project healthy

To use another library, add a line to libraryDependencies and reload sbt. Look up the exact coordinates in the library's documentation or on Maven Central. Commit build.sbt and the project folder to version control, but not the target directories, which hold generated output. Keep tests small and independent, and test the behavior you care about rather than the implementation. As projects grow, you can split into several modules in the same build, and later lessons on Spark will use exactly this setup to package jobs for a cluster.

// build.sbt: add a dependency, then run "reload" in the sbt shell
libraryDependencies += "com.lihaoyi" %% "upickle" % "4.0.2"

// .gitignore
// target/
// project/target/
// .bsp/

FAQs

What is the difference between sbt and Scala CLI?
Scala CLI is ideal for single files, scripts, and small tools, with dependencies declared at the top of the file. sbt is the long-standing build tool for larger multi-file and multi-module projects. Both can compile and test Scala 3, and you can start with Scala CLI and move to sbt as a project grows.
Why does sbt feel slow the first time?
The first run downloads sbt itself, the Scala compiler, and your dependencies. These are cached afterwards. Using the interactive shell or the tilde watch mode avoids restarting the JVM for every command.
Which test library should I use?
munit is simple and gives clear failure messages, and ScalaTest and specs2 are also widely used. The ideas are the same: small named tests with assertions. Pick whichever your team uses, and learn the others when you need to.

Summary

You created a Scala 3 project with sbt, learned the layout and the role of build.sbt, wrote application code as testable pure functions, and tested it with munit using assertEquals, tolerance checks, and intercept. With these tools you can move from experiments to maintainable code. From here a natural next step is Apache Spark, whose Scala API builds directly on the collections, case classes, and functions you have practiced in this path.

Related tutorials

Comments

Log in or create a free account to join the discussion.

  • No comments yet. Be the first to comment.