What Do We Need Mocks For?
Learn about the different types of test doubles and how we can use them.
Until now, we’ve been testing small components in isolation as much as possible. Unit testing is essentially about testing units of code (classes, objects, and so on) without any interaction with the rest of the codebase. The problem is that often our units need other units to work. In this lesson, we’re going to discuss a couple of testing styles to limit, as much as possible, interaction between the unit under test and other units.
State testing vs. behavior testing
When it comes to testing units together, we have two major ways to design our tests: state verification and behavior verification. To show their differences, let’s add a new feature to our small program: the possibility of publishing a course. A course can be published only if it has more than one lesson.
sealed trait Course:val title: Stringval author: Authorval lessons: Set[Lesson]val tags: Set[String]val price: BigDecimalval published: Booleanrequire(lessons.nonEmpty || !published, "A course can be published only if it has at least one lesson")def copy(title: String = title,author: Author = author,lessons: Set[Lesson] = lessons,tags: Set[String] = tags,published: Boolean = published): Coursecase class FreeCourse(override val title: String,override val author: Author,override val lessons: Set[Lesson],override val tags: Set[String],override val published: Boolean) extends Course:override val price: BigDecimal = ??? // as beforecase class PaidCourse(override val title: String,override val author: Author,override val lessons: Set[Lesson],override val tags: Set[String],override val published: Boolean) extends Course:override val price: BigDecimal = ??? // as beforeclass Educative(val courses: Seq[Course]):// rest of Educative as beforedef publishCourse(title: String): Educative = Educative(courses map { c =>if c.title == title && c.lessons.nonEmpty && !c.published then c.copy(published = true)else c})
The implementation of the publishCourse
method simply goes through the collection of courses
, looking for one with the specified title. If such a Course
exists and has at least one lesson, the course is replaced with one instance with the published
flag set. Notice that publishing the same Course
twice will not replace it the second time because the if
condition makes sure the Course
was not published already. Furthermore, to call copy
, we have to explicitly define an abstract method with the same name in the Course
trait.
State testing
The first approach, known as state verification, involves making assertions of the final state of the unit under test, making sure it’s what we expect.
package mdipirro.educative.io.effectiveunitandintegrationtestinginscala.mocks import mdipirro.educative.io.effectiveunitandintegrationtestinginscala.TestSuite import mdipirro.educative.io.effectiveunitandintegrationtestinginscala.model.v3.{Author, Educative, FreeCourse, Lesson, PaidCourse} import org.scalatest.OptionValues class StateVerification extends TestSuite with OptionValues: "Publishing a course with no lessons" `should` "leave the course-base as is" in new Fixture { educative .publishCourse("Scala for Dummies") .courseByName("Scala for Dummies").value.published shouldBe false } "Publishing a course with some lessons" `should` "update the course-base" in new Fixture { educative .publishCourse("Advanced Scala") .courseByName("Advanced Scala").value.published shouldBe true } trait Fixture: val educative: Educative = Educative( Seq( FreeCourse("Scala for Dummies", Author("Mary", "Jane"), Set.empty, Set.empty), PaidCourse("Advanced Scala", Author("Mary", "Jane"), Set(Lesson("Introduction")), Set.empty) ) )
In the test suite above, we first invoke the Educative#publishCourse
method (lines 10 and 16) on an instance of Educative
(a data fixture). We then rely on the result of Educative#courseByName
(chained with OptionsValues#value
) ...
Get hands-on with 1400+ tech skills courses.