Статьи

Разработка современных приложений с помощью Scala: тестирование

Эта статья является частью нашего академического курса под названием « Разработка современных приложений с помощью Scala» .

В этом курсе мы предоставляем среду и набор инструментов, чтобы вы могли разрабатывать современные приложения Scala. Мы охватываем широкий спектр тем: от сборки SBT и реактивных приложений до тестирования и доступа к базе данных. С нашими простыми учебными пособиями вы сможете запустить и запустить собственные проекты за минимальное время. Проверьте это здесь !

1. Введение

В этом разделе руководства мы поговорим о средах тестирования, которые широко применяются большинством разработчиков приложений Scala . Хотя горячие споры об эффективности и полезности практики разработки на основе тестов (или просто TDD ) продолжаются годами, этот раздел основан на истинном убеждении, что тесты — это отличная вещь, которая делает нас лучшими разработчиками программного обеспечения и улучшает качество и ремонтопригодность программных систем, над которыми мы работали или над которыми работаем.

Зачем говорить о тестировании так рано? Что ж, большинство будущих разделов познакомит нас с различными фреймворками и библиотеками, и, как правило, мы собираемся потратить некоторое время на обсуждение стратегий тестирования, применимых в их контексте. Кроме того, это гораздо более простой и безопасный способ продемонстрировать новые возможности языка или инфраструктуры для вашей команды (или организации), если это никак не влияет на производственный код.

2. ScalaCheck: сила тестирования на основе свойств

Первый фреймворк, к которому мы обратимся, — это ScalaCheck, который предназначен для автоматического тестирования на основе свойств. В случае, если вы не знакомы с тестированием на основе свойств, это довольно интуитивный и мощный метод: он в основном проверяет (или, точнее сказать, проверяет), что утверждения о результатах вывода вашего кода верны для ряда автоматически сгенерированных входных данных ,

Давайте начнем с очень простого примера класса Case, чтобы идея и преимущества тестирования на основе свойств стали очевидными.

1
2
3
case class Customer(firstName: String, lastName: String) {
  def fullName = firstName + " " + lastName
}

По сути, традиционный способ обеспечить сбор полного имени из имени и фамилии состоит в том, чтобы написать контрольный пример (или несколько контрольных примеров), явно указав значения имени и фамилии. С ScalaCheck (и тестированием на основе свойств) это выглядит намного проще:

1
2
3
4
5
6
7
object CustomerSpecification extends Properties("Customer") {
  property("fullName") = forAll { (first: String, last: String) =>
    val customer = Customer(first, last)
    customer.fullName.startsWith(first) &&
      customer.fullName.endsWith(last)
  }
}

Входные данные для имени и фамилии генерируются автоматически, в то время как единственное, что нам нужно сделать, — это создать экземпляр класса Customer и определить наши проверки, которые должны быть истинными для всех возможных входных данных:

1
2
customer.fullName.startsWith(first) &&
  customer.fullName.endsWith(last)

Выглядит очень просто, но мы можем сделать даже лучше, чем это. ScalaCheck по умолчанию предоставляет генераторы для всех примитивных типов, давая возможность определять свои собственные. Давайте сделаем еще один шаг и определим генератор для экземпляров класса Customer .

1
2
3
4
5
6
implicit val customerGen: Arbitrary[Customer] = Arbitrary {
  for {
    first <- Gen.alphaStr
    last <- Gen.alphaStr
  } yield Customer(first, last)
}

С этим генератором наш тестовый пример становится еще более простым и читаемым:

1
2
3
4
property("fullName") = forAll { customer: Customer =>
  customer.fullName.startsWith(customer.firstName) &&
    customer.fullName.endsWith(customer.lastName)
}

Очень элегантно, не правда ли? Конечно, ScalaCheck может предложить гораздо больше в плане поддержки гибких проверок (и отчетов об ошибках проверки) для довольно сложных сценариев тестирования, как мы увидим в следующих разделах этого руководства. Тем не менее, официальная документация содержит ряд полезных ссылок на тот случай, если вы захотите узнать больше о ScalaCheck .

Наличие контрольных примеров — это хорошо, но как их запустить? Существует несколько способов запуска тестов ScalaCheck, но наиболее удобным из них является использование SBT , инструмента, о котором мы узнали в первом разделе этого руководства, с использованием его задачи тестирования .

1
2
3
4
5
6
7
$ sbt test
...
[info] + Customer.fullName: OK, passed 100 tests.
[info] + CustomerGen.fullName: OK, passed 100 tests.
[info] Passed: Total 2, Failed 0, Errors 0, Passed 2
 
[success] Total time: 1 s

Как показывает вывод SBT , ScalaCheck сгенерировал 100 различных тестов из нашего определения одного теста, что сэкономило довольно много времени.

3. ScalaTest: тесты как спецификации

ScalaTest является отличным примером основанного на Scala варианта полноценной среды тестирования, которую можно найти во многих других языках (таких как Spock Framework или RSpec, если назвать только некоторые). В основе ScalaTest лежат спецификации тестов, которые поддерживают разные стили написания тестов. На самом деле, это разнообразие стилей тестирования является чрезвычайно полезной функцией, поскольку позволяет разработчикам, приходящим в вселенную Scala с других языков, следовать стилю, с которым они, возможно, уже знакомы и комфортны.

Прежде чем закатывать рукава и исследовать ScalaTest , стоит упомянуть, что текущая стабильная ветка релиза — 2.2, но следующая версия 3.0 скоро будет выпущена (мы надеемся) очень скоро, уже на стадии RC3 . Таким образом, последняя версия 3.0-RC3 станет версией ScalaTest, на которой мы будем строить наши тесты.

Давайте вернемся к классу кейсов Customer и продемонстрируем различные варианты спецификаций ScalaTest , начиная с базового, FlatSpec :

1
2
3
4
5
class CustomerFlatSpec extends FlatSpec {
  "A Customer" should "have fullName set" in {
    assert(Customer("John", "Smith").fullName == "John Smith")
  }
}

Хотя использование утверждений известно и понятно большинству из нас, существуют лучшие, более удобные для человека способы выразить ваши ожидания. В ScalaTest есть соответствующие устройства, которые служат этой цели. Давайте посмотрим на другую версию теста, все еще используя FlatSpec :

1
2
3
4
5
class CustomerMatchersFlatSpec extends FlatSpec with Matchers {
  "A Customer" should "have fullName set" in {
    Customer("John", "Smith").fullName should be ("John Smith")
  }
}

С небольшим изменением, когда явное утверждение было заменено на удобное, должно быть совпадение, обеспечиваемое чертой Matchers , тестовый пример теперь можно свободно читать как историю.

Помимо FlatSpec , ScalaTest включает в себя множество других привилегий в форме FunSuite , FunSpec , WordSpec , FreeSpec , Spec , PropSpec и FeatureSpec . Вам, безусловно, рекомендуется взглянуть на них и выбрать свой любимый, но в конце мы обсудим FeatureSpec .

Разработка, основанная на поведении (или просто BDD ) — это еще одна методология тестирования, которая появилась из TDD и стала довольно популярной в последние годы. В BDD тестовые сценарии (или, лучше сказать, критерии приемки) должны быть написаны для каждой разрабатываемой функции и должны следовать структуре « дано / когда / тогда» . ScalaTest поддерживает этот вид тестирования в стиле FeatureSpec . Итак, давайте взглянем на BDD- версию тестового сценария, который мы реализовали до сих пор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class CustomerFeatureSpec extends FeatureSpec with GivenWhenThen with Matchers {
  info("As a Customer")
  info("I should have my Full Name composed from first and last names")
   
  feature("Customer Full Name") {
    scenario("Customer has correct Full Name representation") {
      Given("A Customer with first and last name")
      val customer = Customer("John", "Smith")
      When("full name is queried")
      val fullName = customer.fullName
      Then("first and last names should be returned")
      fullName should be ("John Smith")
    }
  }
}

Конечно, для такого простого сценария тестирования стиль BDD может выглядеть излишним, но на самом деле это очень выразительный способ написания критериев приемлемости, которые могут быть понятны как бизнесменам, так и инженерам одновременно.

Без сомнения, ScalaTest имеет первоклассную поддержку SBT и, имея все наши тестовые наборы, давайте запустим их, чтобы убедиться, что они все прошли:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
$ sbt test
...
[info] CustomerFeatureSpec:
[info] As a Customer
[info] I should have my Full Name composed from first and last names
[info] Feature: Customer Full Name
[info]   Scenario: Customer has correct Full Name representation
[info]     Given A Customer with first and last name
[info]     When full name is queried
[info]     Then first and last names should be returned
[info] CustomerMatchersFlatSpec:
[info] A Customer
[info] - should have fullName set
[info] CustomerFlatSpec:
[info] A Customer
[info] - should have fullName set
[info] Run completed in 483 milliseconds.
[info] Total number of tests run: 3
[info] Suites: completed 3, aborted 0
[info] Tests: succeeded 3, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.
[success] Total time: 1 s

Как мы видим из вывода SBT , набор тестов FeatureSpec форматирует и распечатывает описания функций и сценариев, а также подробную информацию о каждом выполненном шаге « Задано / Когда / Затем» .

4. Specs2: один, чтобы управлять ими всеми

Последняя (но не менее важная ) среда тестирования Scala, которую мы собираемся обсудить в этом разделе, — это specs2 . С точки зрения возможностей, это довольно близко к ScalaTest, но specs2 использует немного другой подход. По сути, он также основан на тестовых спецификациях, но есть только два из них: изменяемые (или спецификации единиц ) и неизменяемые (или приемочные спецификации ).

Давайте посмотрим, что на самом деле означают эти два стиля, разработав тестовые наборы одного и того же типа для нашего класса примеров Customer .

1
2
3
4
5
6
7
class CustomerSpec extends Specification {
  "Customer" >> {
    "full name should be composed from first and last names" >> {
      Customer("John", "Smith").fullName must_== "John Smith"
    }
  }
}

Похоже, довольно простой набор тестов со знакомой структурой. Не сильно отличается от ScalaTest за исключением того, что расширяет класс org.specs2.mutable.Specification . Стоит отметить, что specs2 имеет чрезвычайно богатый набор соответствий , предоставляя, как правило, разные их варианты в зависимости от вашего предпочтительного стиля. В нашем случае мы используем must_== matcher.

Другой способ создания тестовых наборов с помощью specs2 — расширить класс org.specs2.Specification и записать спецификацию в виде чистого текста. Например:

1
2
3
4
5
6
7
8
class CustomerFeatureSpec extends Specification { def is = s2"""
  Customer
    should have full name composed from first and last names $fullName
  """ 
     
  val customer = Customer("John", "Smith")
  def fullName = customer.fullName must_== "John Smith"
}

Этот вид спецификаций стал возможен после введения возможностей интерполяции строк в языке Scala и компиляторе. Как вы уже могли догадаться, такой удобный способ определения спецификаций тестов — отличная возможность для написания тестовых примеров в стиле BDD . Конечно, specs2 позволяет это сделать, но идет еще дальше, предоставляя дополнительную поддержку с точки зрения шагов и сценариев. Давайте посмотрим на пример такой спецификации теста.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
class CustomerGivenWhenThenSpec extends Specification
  with GWT with StandardDelimitedStepParsers { def is = s2"""
   As a Customer
   I should have my Full Name composed from first and last names  ${scenario.start}
   Given a customer last name {Smith}
   And first name {John}
   When I query for full name
   Then I should get: {John Smith}                                ${scenario.end}
  """ 
     
  val scenario =
    Scenario("Customer")
      .given(aString)
      .given(aString)
      .when() { case s :: first :: last :: _ =>
        Customer(first, last)
      }
      .andThen(aString) { case expected :: customer :: _ =>
        customer.fullName must be_== (expected)
      }
}

По сути, в этом случае спецификация теста состоит из двух частей: фактического определения сценария и его интерпретации (иногда называемого подходом с выделением кода). Имя и фамилия извлекаются из определения (с использованием пошаговых анализаторов), экземпляр класса дела клиента создается в фоновом режиме, а на последнем шаге ожидаемое полное имя также извлекается из определения и сравнивается с именем клиента. Если вы находите это немного громоздким, это не единственный способ определить спецификации стиля Given / When / Then с использованием specs2 . Но имейте в виду, что другие альтернативы потребуют от вас отслеживать и поддерживать состояние между различными шагами задано / когда / затем .

И в завершение specs2 было бы неполно не упомянуть его бесшовную интеграцию с фреймворком ScalaCheck, просто расширив черту org.specs2.ScalaCheck , например:

01
02
03
04
05
06
07
08
09
10
11
class CustomerPropertiesSpec extends Specification with ScalaCheck { def is = s2"""
  Customer
    should have full name composed from first and last names ${fullName}
  """ 
     
  val fullName: Prop = forAll { (first: String, last: String) =>
    val customer = Customer(first, last)
    customer.fullName.startsWith(first) &&
      customer.fullName.endsWith(last)
  }
}

Результат — именно то, что вы ожидаете от запуска чистого ScalaCheck : проверки будут выполняться для сгенерированных имен и фамилий. Используя нашего друга SBT , давайте запустим все тестовые наборы, чтобы убедиться, что мы хорошо поработали.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ sbt test
...
[info] CustomerPropertiesSpec
[info]     + Customer should have full name composed from first and last names
[info]
[info] Total for specification CustomerPropertiesSpec
[info] Finished in 196 ms
[info] 1 example, 100 expectations, 0 failure, 0 error
[info]
[info] CustomerSpec
[info]
[info] Customer
[info]   + full name should be composed from first and last names
[info]
[info]
[info] Total for specification CustomerSpec
[info] Finished in 85 ms
[info] 1 example, 0 failure, 0 error
[info]
[info] CustomerFeatureSpec
[info]   Customer
[info]     + should have full name composed from first and last names
[info]
[info] Total for specification CustomerFeatureSpec
[info] Finished in 85 ms
[info] 1 example, 0 failure, 0 error
[info]
[info] CustomerGivenWhenThenSpec
[info]    As a Customer
[info]    I should have my Full Name composed from first and last names
[info]
[info]    Given a customer last name Smith
[info]    And first name John
[info]    When I query for full name
[info]    + Then I should get: John Smith
[info]
[info] Total for specification CustomerGivenWhenThenSpec
[info] Finished in 28 ms
[info] 1 example, 4 expectations, 0 failure, 0 error
[info] Passed: Total 4, Failed 0, Errors 0, Passed 4
[success] Total time: 10 s

Вывод с консоли SBT очень похож на то, что мы видели для ScalaTest , со всеми подробностями комплектов тестов, напечатанными в компактном и читаемом формате.

5. Выводы

Scala — чрезвычайно мощный язык, предназначенный для решения самых разнообразных задач разработки программного обеспечения. Но, как и любая другая программа, написанная на множестве языков программирования, приложения Scala не являются неизменными для ошибок. Практики TDD (и в последнее время BDD ) широко применяются сообществом разработчиков Scala , что привело к созданию таких великолепных сред тестирования, как ScalaCheck , ScalaTest и specs2 .

6. Что дальше

В следующем разделе руководства мы поговорим о реактивных приложениях, в частности сосредоточим наше внимание на реактивных потоках как на их прочной основе, а также кратко коснемся «Реактивного манифеста» .