Тестирование в Scala с помощью ScalaTest и определение степени покрытия кода тестами
6 июля 2015
Несмотря на то, что Scala является языком со строгой статической типизацией, что устраняет множество ошибок в коде еще на этапе компиляции, это не отменяет необходимости писать тесты. Ведь нормально проверить логику работы вашего приложения могут только тесты. Среди великого множества тестовых фреймворков для Scala довольно большой популярностью пользуется ScalaTest. С ним мы сегодня и познакомимся.
Помните, мы писали простой REST-сервис на Finagle? Внимательные читатели могли обратить внимание, что в репозитории с проектом также имеются и тесты, проверяющие работу сервиса, обращаясь к нему через Finagle’овский HTTP клиент.
Так к проекту подключается ScalaTest:
Обратите внимание на часть % "test"
. Она говорит, что пакет нужен только для выполнения тестов, и что его не нужно включать в само приложение.
А вот так выглядит один из тестов:
import com.twitter.finagle.{Http, Service}
import com.twitter.util.Await
import me.eax.finagle_example.FinagleServiceExample
import org.jboss.netty.buffer.ChannelBuffers
import org.jboss.netty.handler.codec.http._
import org.jboss.netty.util.CharsetUtil
import org.scalatest._
class RestSpec extends FunSpec with Matchers {
val service = new FinagleServiceExample
val server = Http.serve(":8080", service)
val client: Service[HttpRequest, HttpResponse] = {
Http.newService("localhost:8080")
}
describe("Server") {
// ... skipped ...
it("returns 'bad request' for bad requests") {
val request = {
new DefaultHttpRequest(
HttpVersion.HTTP_1_1,
HttpMethod.PUT,
"/some-key"
)
}
val response = Await.result(client(request))
response.getStatus shouldBe HttpResponseStatus.BAD_REQUEST
}
}
}
На что тут нужно обратить внимание:
Подключаем ScalaTest.
Набор тестов — это обычный класс, в который подмешивается несколько трейтов из ScalaTest. В ScalaTest предусмотрено больше одного DSL для написания тестов (в их терминологии — testing styles). В рамках данной заметки мы сосредоточим свое внимание на стиле FunSpec, хотя вам, возможно, больше по вкусу придется один из других стилей. Трейт Matchers позволяет использовать в тестах комбинаторы вроде shouldBe вместо обычного assert. Как вы сможете скоро убедиться, благодаря Matchers код тестов действительно становится более читаемым.
it("does something") {
// ...
}
it("does something else") {
// ...
}
}
При использовании FunSpec тесты организуются в иерархическую структуру из describe-блоков и вложенных it-блоков. Кроме того, внутри describe’ов могут быть и другие describe’ы.
Фактически, выражение x shouldBe y
эквивалентно assert(x == y)
. Кроме того, предусмотрен ряд других полезных комбинаторов. Рассмотрим некоторые из них.
something should be > 0
Проверка больше, меньше, больше или равно, и так далее.
Проверка, что коллекция не пустая — лучше, чем проверять .size
.
something should not contain beta
Убеждаемся, что коллекция содержит или не содержит заданный элемент.
Проверка, что переменная хранит определенный тип.
Паттерн матчинг.
double1 shouldBe double2 +- eps
Сравнение значений с точностью до заданной эпсилон.
// ...
}
Проверяем, что выражение бросает указанное исключение.
val foo = intercept[Exception] {
// ...
}
foo.bar shouldBe baz
Проверяем содержимое пойманного исключения.
class MySpec extends FunSpec with Matchers with Eventually {
describe("aaa") {
it("bbb") {
eventually {
// ...
}
eventually(timeout(2.seconds)) {
// ...
}
}
}
}
Проверка, что какое-то условие будет выполнено в конечном счете. Можно указать время ожидания, а также через какие интервалы производить проверки. Очень удобно, когда приложение делает что-то в фоне и неизвестно точно, когда пользователю станет доступен результат. Не правда ли, этот вариант будет получше, чем Thread.sleep(1000)
?
Если вы делаете какой-то сложный рефакторинг, из-за которого некий тест постоянно падает, можно временно его отключить:
ignore("does something") { // FIXME!
// ...
}
}
Главное — чтобы такое по ошибке не попадало в основную ветку, потому что потом времени исправить тесты ни у кого не находится!
Пример запуска тестов из консоли:
... skipped ...
[info] RestSpec:
[info] Server
[info] - returns 'not found' for non-exiting key
[info] - saves, returns and deletes values
[info] - returns 'bad request' for bad requests
[info] Run completed in 1 second, 203 milliseconds.
[info] Total number of tests run: 3
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 3, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
При наличии множества тестов удобно иметь возможность запустить только конкретный:
Можно даже запустить не всю спеку, а только отдельный тест из нее:
scala> import me.eax.finagle_example.tests._
scala> val spec = new RestSpec;
scala> spec.execute(
| testName = "Server returns 'bad request' for bad requests"
| )
RestSpec:
Server
- returns 'bad request' for bad requests
Или даже множество тестов, у которых имя начинается одинаково:
RestSpec:
Server
- returns 'not found' for non-exiting key
- returns 'bad request' for bad requests
Почти то же самое прямо из bash:
Запускает все тесты, у которых имя где угодно (то есть, не только в начале) содержит «saves».
Бывает удобно параметризовать тесты через консоль. Например, указать путь до каких-нибудь тестовый данных на диске, переключиться между тестированием на реальной базе данных и моком, и так далее. Это можно сделать, перегрузив метод runTests:
val useRealDB = args.configMap
.getOrElse("db.use.real", "false")
.asInstanceOf[String]
// ...
}
Передача аргументов почему-то работает только через test-only, не через test:
В IntelliJ IDEA эти параметры можно прописать в Run → Edit Configurations → Configuration → Test options.
С помощью плагина:
… можно померить степень покрытия кода тестами:
Плагин пишет отчеты о покрытии в форматах HTML и XML, а также выводит степень покрытия в консоль:
[info] All done. Coverage was [73.91%]
Можно считать сборку неуспешной, если уровень покрытия кода тестами падает ниже заданного уровня:
ScoverageSbtPlugin.ScoverageKeys.coverageFailOnMinimum := true
Только примите во внимание, что на практике этот уровень может немного прыгать от запуска к запуску, даже если вы не меняли код.
Лишние пакеты и классы при желании можно заэксклудить:
При работе со ScalaTest было замечено, что IntelliJ IDEA по умолчанию запускает тесты последовательно, один за одним, в то время, как SBT запускает все тесты параллельно. В результате тесты нормально выполняются при запуске из IDE и падают в TeamCity. Если вы столкнетесь с этой проблемой, допишите в build.sbt:
Многие возможности ScalaTest, к сожалению, выходят за рамки данной заметки. Как уже отмечалось, фреймворк поддерживает множество DSL. Также в нем есть тэги, фикстуры, поддержка property-based тестов и много чего еще. Подробности можно найти в ScalaTest User Guide. В частности, про матчеры более подробно рассказывается здесь. В случае возникновения проблем со ScalaTest всегда можно попросить помощи в их списке рассылки.
Метки: Scala, Тестирование, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.