Совместное использование элементов между сгенерированными объектами в ScalaCheck с использованием вложенного forAll

Начал программировать на Scala совсем недавно и пытался написать тестовые примеры на основе свойств. Здесь я пытаюсь сгенерировать необработанные данные, которые имитируют тестируемую систему. Цель состоит в том, чтобы сначала создать базовые элементы (ctrl и idz), затем использовать эти значения для создания двух классов (A1 и B1) и, наконец, проверить их свойства. Сначала я попробовал следующее -

import org.scalatest._
import prop._
import scala.collection.immutable._
import org.scalacheck.{Gen, Arbitrary}

case class A(
    controller: String,
    id: Double,
    x: Double
)

case class B(
    controller: String,
    id: Double,
    y: Double
)

object BaseGenerators {
    val ctrl = Gen.const("ABC")
    val idz = Arbitrary.arbitrary[Double]
}

trait Generators {
    val obj = BaseGenerators

    val A1 = for {
        controller <- obj.ctrl
        id <- obj.idz
        x <- Arbitrary.arbitrary[Double]
    } yield A(controller, id, x)

    val B1 = for {
        controller <- obj.ctrl
        id <- obj.idz
        y <- Arbitrary.arbitrary[Double]
    } yield B(controller, id, y)

}

class Something extends PropSpec with PropertyChecks with Matchers with Generators{

    property("Controllers are equal") {
        forAll(A1, B1) {
            (a:A,b:B) => 
                a.controller should be (b.controller)
        }
    }

    property("IDs are equal") {
        forAll(A1, B1) {
            (a:A,b:B) => 
                a.id should be (b.id)
        }
    }

}

Запуск sbt test в терминале дал мне следующее:

[info] Something:
[info] - Controllers are equal
[info] - IDs are equal *** FAILED ***
[info]   TestFailedException was thrown during property evaluation.
[info]     Message: 1.1794559135007427E-271 was not equal to 7.871712821709093E212
[info]     Location: (testnew.scala:52)
[info]     Occurred when passed generated values (
[info]       arg0 = A(ABC,1.1794559135007427E-271,-1.6982696700585273E-23),
[info]       arg1 = B(ABC,7.871712821709093E212,-8.820696498155311E234)
[info]     )

Теперь легко понять, почему второе свойство не работает. Потому что каждый раз, когда я возвращаю A1 и B1, я получаю другое значение для id, а не для ctrl, потому что это константа. Ниже приведен мой второй подход, в котором я создаю вложенные for-yield, чтобы попытаться достичь своей цели:

case class Popo(
    controller: String,
    id: Double,
    someA: Gen[A],
    someB: Gen[B]
)

trait Generators {
    val obj = for {
        ctrl <- Gen.alphaStr
        idz <- Arbitrary.arbitrary[Double]
        val someA = for {
            x <- Arbitrary.arbitrary[Double]
        } yield A(ctrl, idz, someA)
        val someB = for {
            y <- Arbitrary.arbitrary[Double]
        } yield B(ctrl, idz, y)
    } yield Popo(ctrl, idz, x, someB)
}

class Something extends PropSpec with PropertyChecks with Matchers with Generators{

    property("Controllers are equal") {
        forAll(obj) {
            (x: Popo) => 
            forAll(x.someA, x.someB) {
                (a:A,b:B) => 
                    a.controller should be (b.controller)
            }
        }
    }

    property("IDs are equal") {
        forAll(obj) {
            (x: Popo) =>
            forAll(x.someA, x.someB) {
                (a:A,b:B) => 
                    a.id should be (b.id)
            }
        }
    }
}

Выполнение sbt test во втором подходе говорит мне, что все тесты пройдены.

[info] Something:
[info] - Controllers are equal
[info] - IDs are equal
[info] ScalaTest
[info] Run completed in 335 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

Есть ли лучший/альтернативный способ воспроизвести желаемые результаты? Вложение forAll кажется мне довольно неуклюжим. Если бы у меня было R -> S -> ... V -> W в моем графе зависимостей для общих элементов объектов, мне пришлось бы создать столько же вложенных forAll.


person Ic3fr0g    schedule 31.05.2019    source источник
comment
Трудно сказать, каковы ваши свойства на самом деле. Например, "IDs are equal", кажется, говорит, что любая пара A и B должна иметь одно и то же id, что не кажется значимым свойством. Я предполагаю, что вы хотите иметь некоторый тип данных, который имеет элементы A и B (а не Gen[A] и Gen[B]), и тогда ваши свойства будут касаться отношений между членами этого типа данных.   -  person Travis Brown    schedule 31.05.2019
comment
Хорошо, прямо сейчас свойства-тесты являются заполнителем. Что я в конечном итоге хочу проверить, так это то, что для каждой пары случайно сгенерированных A и B, где controller и id одинаковы, удовлетворяет ли функция GaGa допустимому свойству. Итак, в каком-то смысле мне нужен тип данных, который создает Gen[A], Gen[B] для проверки правильности свойств GaGa.   -  person Ic3fr0g    schedule 31.05.2019
comment
Извините, это все еще очень непонятно для меня. Я думаю, у вас больше шансов получить полезный ответ с более полным и минимальным примером.   -  person Travis Brown    schedule 31.05.2019
comment
Хорошо, я отредактирую вопрос в ближайшие пару часов, чтобы лучше проиллюстрировать его.   -  person Ic3fr0g    schedule 31.05.2019


Ответы (1)


Я собираюсь дать ответ только в Scalacheck. Я знаю, что Scalatest популярен, но я считаю, что его включение в вопрос о Scalacheck отвлекает, особенно когда нет причин, по которым пример нельзя было бы написать без него.

Кажется, вы хотите протестировать A и B, но они обмениваются информацией. Одним из способов представления этой зависимости может быть написанный вами класс Popo. Он содержит как общую информацию, так и сгенерированные значения A и B. Другой вариант — создание общих значений между A и B в классе.

Самое простое решение — генерировать A и B парами (кортеж из двух). К сожалению, есть некоторые хитрости, чтобы заставить его работать. Вам нужно будет использовать ключевое слово case в свойстве forAll. Вы не можете указать значение implicit для Arbitrary кортежей, поэтому вы должны явно указать генератор для кортежей в файле forAll.

import org.scalacheck.Gen
import org.scalacheck.Arbitrary
import org.scalacheck.Prop
import org.scalacheck.Prop.AnyOperators
import org.scalacheck.Properties

case class A(
  controller: String,
  id: Double,
  x: Double
)

case class B(
  controller: String,
  id: Double,
  y: Double
)

object BaseGenerators {
  val ctrl = Gen.const("ABC")
  val idz = Arbitrary.arbitrary[Double]
}

object Generators {
  val obj = BaseGenerators

  val genAB: Gen[(A,B)] = for {
    controller <- obj.ctrl
    id <- obj.idz
    x <- Arbitrary.arbitrary[Double]
    y <- Arbitrary.arbitrary[Double]
    val a = A(controller, id, x)
    val b = B(controller, id, y)
  } yield (a, b)                                         // !
}

class Something extends Properties("Something") {

  property("Controllers and IDs are equal") = {
    Prop.forAll(Generators.genAB) { case (a: A, b: B) => // !
      a.controller ?= b.controller && a.id ?= b.id
    }
  }
}

Что касается вашего более широкого вопроса о наличии объектов, которые обмениваются информацией, вы можете представить ее, написав свои генераторы с аргументами функции. Однако для этого все равно потребуются вложенные генераторы forAll.

object Generators {
  val obj = BaseGenerators

  val genA = for {
    controller <- obj.ctrl
    id <- obj.idz
    x <- Arbitrary.arbitrary[Double]
  } yield A(controller, id, x)

  def genB(a: A) = for {                                 // !
    y <- Arbitrary.arbitrary[Double]
  } yield B(a.controller, a.id, y)
}

class Something extends Properties("Something") {

  implicit val arbA: Arbitrary[A] = Arbitrary {
    Generators.genA
  }

  property("Controllers and IDs are equal") = {
    Prop.forAll { a: A =>                                // !
      Prop.forAll(Generators.genB(a)) { b: B =>          // !
        (a.controller ?= b.controller) && (a.id ?= b.id)
      }
    }
  }
}
person ashawley    schedule 03.06.2019