Як написати модульні тести в Spark 2.0+?


78

Я намагався знайти розумний спосіб тестування SparkSessionза допомогою тестової системи JUnit. Хоча, мабуть, є хороші приклади SparkContext, я не міг зрозуміти, як отримати відповідний приклад, для якого він працює SparkSession, хоча він використовується в декількох місцях всередині бази тестування іскр . Я був би радий спробувати рішення, в якому також не використовується база для тестування іскр, якщо це насправді не правильний шлях сюди.

Простий тест ( повний MWE проект з build.sbt):

import com.holdenkarau.spark.testing.DataFrameSuiteBase
import org.junit.Test
import org.scalatest.FunSuite

import org.apache.spark.sql.SparkSession


class SessionTest extends FunSuite with DataFrameSuiteBase {

  implicit val sparkImpl: SparkSession = spark

  @Test
  def simpleLookupTest {

    val homeDir = System.getProperty("user.home")
    val training = spark.read.format("libsvm")
      .load(s"$homeDir\\Documents\\GitHub\\sample_linear_regression_data.txt")
    println("completed simple lookup test")
  }

}

Результатом запуску цього за допомогою JUnit є NPE на лінії завантаження:

java.lang.NullPointerException
    at SessionTest.simpleLookupTest(SessionTest.scala:16)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:237)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Зауважте, не повинно мати значення, що файл, який завантажується, існує чи ні; у правильно налаштованій SparkSession буде викинуто більш розумну помилку .


1
Дякуємо всім за відповіді на даний момент; Я сподіваюся переглянути незабаром. Я також відкрив проблему і перехресно посилаюся на
bbarker

На жаль, я все ще не дійшов до того, щоб насправді використовувати Spark ... колись, можливо, 3.x із такою швидкістю - інакше я б працював над прийняттям відповіді. Радий, що це було корисно для інших.
bbarker

Відповіді:


112

Дякуємо за те, що ви поставили це вирішальне запитання. Чомусь, коли мова заходить про Spark, кожен настільки захоплюється аналітикою, що забуває про чудові практики програмної інженерії, що з’явилися за останні 15 років або близько того. Ось чому ми ставимо за мету обговорити тестування та постійну інтеграцію (серед іншого, як DevOps) у нашому курсі.

Короткий огляд термінології

Істинний тест блоку означає , що ви маєте повний контроль над кожним компонентом в тесті. Не може бути взаємодії з базами даних, викликами REST, файловими системами або навіть системним годинником; все повинно бути "подвоєно" (наприклад, знущатися над ним, тощо), як Gerard Mezaros вказує це в тестових шаблонах xUnit . Я знаю, що це здається семантикою, але це насправді важливо. Незрозуміння цього є однією з основних причин того, чому ви бачите періодичні невдачі тесту при постійній інтеграції.

Ми все ще можемо юніт-тест

Отже, з огляду на це розуміння, модульне тестування RDDнеможливе. Однак, все ще є місце для модульного тестування при розробці аналітики.

Розглянемо просту операцію:

rdd.map(foo).map(bar)

Тут fooі barє прості функції. Їх можна протестувати в звичайному режимі, і вони повинні бути з якомога більшою кількістю кутових футлярів. Врешті-решт, чому їм цікаво, звідки вони беруть свої вклади - це тестовий прилад чи RDD?

Не забувайте іскрову оболонку

Це не тестування як таке , але на цих ранніх етапах вам також слід експериментувати в оболонці Spark, щоб з’ясувати ваші перетворення та особливо наслідки вашого підходу. Наприклад, ви можете вивчити фізичні і логічні плани запитів, що ділять стратегії і збереження, і стан ваших даних з різними функціями , як toDebugString, explain, glom, show, printSchema, і так далі. Я дозволю вам дослідити їх.

Ви також можете налаштувати свого майстра на local[2]оболонку Spark та у своїх тестах, щоб виявляти будь-які проблеми, які можуть виникнути лише після того, як ви почнете розподіляти роботу.

Тестування інтеграції з Spark

Тепер про цікаві речі.

Для того, щоб перевірити інтеграцію Spark після того, як ви переконаєтесь у якості ваших допоміжних функцій та RDD/ DataFrameлогіки перетворення, критично важливо зробити кілька речей (незалежно від інструменту побудови та тестової основи):

  • Збільште пам’ять JVM.
  • Увімкнути розгалуження, але вимкнути паралельне виконання.
  • Використовуйте ваш тестовий фреймворк, щоб накопичувати свої тести інтеграції Spark у набори, і ініціалізувати SparkContextпопередні тести та зупинити їх після всіх тестів.

З ScalaTest ви можете змішувати BeforeAndAfterAll(що я переважно віддаю перевагу) або BeforeAndAfterEachяк @ShankarKoirala робить, щоб ініціалізувати та зруйнувати артефакти Spark. Я знаю, що це розумне місце, щоб зробити виняток, але мені справді не подобаються ті змінні, varякі ви повинні використовувати.

Шаблон позики

Інший підхід - використання шаблону позики .

Наприклад (за допомогою ScalaTest):

class MySpec extends WordSpec with Matchers with SparkContextSetup {
  "My analytics" should {
    "calculate the right thing" in withSparkContext { (sparkContext) =>
      val data = Seq(...)
      val rdd = sparkContext.parallelize(data)
      val total = rdd.map(...).filter(...).map(...).reduce(_ + _)

      total shouldBe 1000
    }
  }
}

trait SparkContextSetup {
  def withSparkContext(testMethod: (SparkContext) => Any) {
    val conf = new SparkConf()
      .setMaster("local")
      .setAppName("Spark test")
    val sparkContext = new SparkContext(conf)
    try {
      testMethod(sparkContext)
    }
    finally sparkContext.stop()
  }
} 

Як бачите, шаблон позики використовує функції вищого порядку, щоб "позичити" SparkContextтест, а потім розпорядитися ним після закінчення.

Орієнтоване на страждання програмування (Дякую, Натане)

Це повністю питання переваг, але я вважаю за краще використовувати шаблон позики та підключати речі доти, доки зможу, перш ніж залучити іншу структуру. Окрім того, що просто намагаються залишатися легкими, фреймворки іноді додають багато «магії», через яку важко міркувати про помилки налагоджувальних тестів. Тому я приймаю підхід, орієнтований на страждання - де я уникаю додавання нового фреймворку, поки біль від його відсутності не буде занадто великим. Але знову ж таки, це залежить від вас.

Найкращий вибір для цього альтернативного фреймворку - це, звичайно, база для тестування іскр, як згадував @ShankarKoirala. У цьому випадку тест вище виглядатиме так:

class MySpec extends WordSpec with Matchers with SharedSparkContext {
      "My analytics" should {
        "calculate the right thing" in { 
          val data = Seq(...)
          val rdd = sc.parallelize(data)
          val total = rdd.map(...).filter(...).map(...).reduce(_ + _)

          total shouldBe 1000
        }
      }
 }

Зверніть увагу, як мені не потрібно було нічого робити, щоб мати справу з SparkContext. SharedSparkContextдав мені все це - причому scяк SparkContext- безкоштовно. Особисто, хоча я б не залучав цю залежність саме з цією метою, оскільки шаблон позики робить саме те, що мені для цього потрібно. Крім того, з такою великою кількістю непередбачуваності, що трапляється з розподіленими системами, може бути справжньою болем прослідкувати магію, яка трапляється у вихідному коді сторонніх бібліотек, коли при постійній інтеграції все йде не так.

Зараз те, що іспитно-тестова база справді сяє, - це помічники на базі Hadoop, такі як HDFSClusterLikeі YARNClusterLike. Змішування цих рис дійсно може заощадити вам багато болю при налаштуванні. Ще одне місце, де воно блищить, - це властивості та генератори, схожі на Scalacheck - припускаючи, звичайно, ви розумієте, як працює тестування на основі властивостей і чому це корисно. Але знову ж таки, я б особисто затримався з його використанням, поки моя аналітика та мої тести не досягнуть такого рівня витонченості.

"Тільки ситх має справу з абсолютом". - Обі-Ван Кенобі

Звичайно, вам не потрібно обирати те чи інше. Може бути , ви могли б використовувати шаблон підхід позики для більшості ваших тестів і іскрового тестуванням бази тільки для декількох, більш строгих випробувань. Вибір не є двійковим; ви можете зробити і те, і інше.

Тестування інтеграції з іскровим потоком

Нарешті, я хотів би лише представити фрагмент того, як може виглядати налаштування тесту інтеграції SparkStreaming зі значеннями в пам'яті без бази іспитового тестування :

val sparkContext: SparkContext = ...
val data: Seq[(String, String)] = Seq(("a", "1"), ("b", "2"), ("c", "3"))
val rdd: RDD[(String, String)] = sparkContext.parallelize(data)
val strings: mutable.Queue[RDD[(String, String)]] = mutable.Queue.empty[RDD[(String, String)]]
val streamingContext = new StreamingContext(sparkContext, Seconds(1))
val dStream: InputDStream = streamingContext.queueStream(strings)
strings += rdd

Це простіше, ніж здається. Це насправді просто перетворює послідовність даних у чергу для подання до DStream. Більшість з них насправді є лише типовим налаштуванням, яке працює з API Spark. Незважаючи на це, ви можете порівняти це з тим StreamingSuiteBase , що знайдено в базі тестування іскр, щоб вирішити, якому саме ви віддаєте перевагу.

Це може бути мій найдовший допис, тож я залишу його тут. Я сподіваюся, що інші співпрацюють з іншими ідеями, щоб допомогти покращити якість нашої аналітики за допомогою тих самих гнучких практик інженерного програмного забезпечення, які покращили всі інші розробки додатків.

І, вибачившись за безсоромну розетку, ви можете ознайомитися з нашим курсом Analytics з Apache Spark , де ми розглядаємо багато цих ідей та багато іншого. Ми сподіваємось, що найближчим часом з’явиться онлайн-версія.


2
Дякую за цей детальний запис. Бажаю, щоб я міг дати вам більше одного голосу.
user1452132

1
Дякую. Це дуже добре. Сподіваюсь, відповідь допоможе вам із вашим проектом чи розумінням.
Від'я

27

Ви можете написати простий тест за допомогою FunSuite та BeforeAndAfterEach, як показано нижче

class Tests extends FunSuite with BeforeAndAfterEach {

  var sparkSession : SparkSession = _
  override def beforeEach() {
    sparkSession = SparkSession.builder().appName("udf testings")
      .master("local")
      .config("", "")
      .getOrCreate()
  }

  test("your test name here"){
    //your unit test assert here like below
    assert("True".toLowerCase == "true")
  }

  override def afterEach() {
    sparkSession.stop()
  }
}

Вам не потрібно створювати функції в тесті, ви можете просто написати як

test ("test name") {//implementation and assert}

Холден Карау написав справді хорошу тестову базу для тестування іскр

Потрібно перевірити нижче - простий приклад

class TestSharedSparkContext extends FunSuite with SharedSparkContext {

  val expectedResult = List(("a", 3),("b", 2),("c", 4))

  test("Word counts should be equal to expected") {
    verifyWordCount(Seq("c a a b a c b c c"))
  }

  def verifyWordCount(seq: Seq[String]): Unit = {
    assertResult(expectedResult)(new WordCount().transform(sc.makeRDD(seq)).collect().toList)
  }
}

Сподіваюся, це допомагає!


Чудова відповідь. Іскрова специфікація використовується аналогічний підхід, але він був занадто повільним , коли багато тестових файли були додані в проект. Подивіться мою відповідь на альтернативну реалізацію, яка не змушує зупиняти / запускати SparkSession після кожного тестового файлу.
Повноваження

2
Мені також подобається перша частина цієї відповіді; Я просто хотів би, щоб у другому прикладі були речі Spark, а не твердження про іграшку. Окрім цього, я хотів би зазначити, що поняття проведення дорогих побічних ефектів до та / або після набору тестів - не нова ідея. Як я пропоную у своїй відповіді, ScalaTest має для цього широкі механізми - в даному випадку для управління артефактами Spark - і ви можете використовувати їх, як і для будь-яких інших дорогих приладів. Принаймні, поки не настане час, коли варто залучати важчі сторонні рамки.
Відя

Зауважимо, що ScalaTest та specs2 (які, я думаю, роблять це за замовчуванням) можуть паралельно запускати тести для збільшення швидкості. Інструменти побудови також можуть допомогти. Але знову ж таки, нічого з цього не є новим.
Від'я

Я відредагував відповідний тестовий приклад для бази тестування іскр відповідно до вашої пропозиції. Дякую,
koiralo,

17

Починаючи з Spark 1.6, ви можете використовувати SharedSparkContextабо SharedSQLContextте, що Spark використовує для власних модульних тестів:

class YourAppTest extends SharedSQLContext {

  var app: YourApp = _

  protected override def beforeAll(): Unit = {
    super.beforeAll()

    app = new YourApp
  }

  protected override def afterAll(): Unit = {
    super.afterAll()
  }

  test("Your test") {
    val df = sqlContext.read.json("examples/src/main/resources/people.json")

    app.run(df)
  }

Оскільки Spark 2.3 SharedSparkSession доступний:

class YourAppTest extends SharedSparkSession {

  var app: YourApp = _

  protected override def beforeAll(): Unit = {
    super.beforeAll()

    app = new YourApp
  }

  protected override def afterAll(): Unit = {
    super.afterAll()
  }

  test("Your test") {
    df = spark.read.json("examples/src/main/resources/people.json")

    app.run(df)
  }

ОНОВЛЕННЯ:

Залежність від Maven:

<dependency>
  <groupId>org.scalactic</groupId>
  <artifactId>scalactic</artifactId>
  <version>SCALATEST_VERSION</version>
</dependency>
<dependency>
  <groupId>org.scalatest</groupId>
  <artifactId>scalatest</artifactId>
  <version>SCALATEST_VERSION</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-core</artifactId>
  <version>SPARK_VERSION</version>
  <type>test-jar</type>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.apache.spark</groupId>
  <artifactId>spark-sql</artifactId>
  <version>SPARK_VERSION</version>
  <type>test-jar</type>
  <scope>test</scope>
</dependency>

Залежність від SBT:

"org.scalactic" %% "scalactic" % SCALATEST_VERSION
"org.scalatest" %% "scalatest" % SCALATEST_VERSION % "test"
"org.apache.spark" %% "spark-core" % SPARK_VERSION % Test classifier "tests"
"org.apache.spark" %% "spark-sql" % SPARK_VERSION % Test classifier "tests"

Крім того, ви можете перевірити тестові джерела Spark, де є величезний набір різноманітних тестових костюмів.

ОНОВЛЕННЯ 2:

Тестування іскрового блоку Apache Частина 1 - Основні компоненти

Apache Spark Unit Testing Part 2 - Spark SQL

Apache Spark Unit Testing Part 3 - Streaming

Тестування інтеграції Apache Spark


1
ви знаєте, який пакет maven містить цей клас?
Джеймс Ган

Звичайно. І те, і "org.apache.spark" %% "spark-sql" % SPARK_VERSION % Test classifier "tests"
інше

For Maven<dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-sql</artifactId> <version>SPARK_VERSION</version> <type>test-jar</type> <scope>test</scope> </dependency>
Євген Лопаткін

4
Для мене також потрібно було додати джерела іскрового сердечника та іскрового каталізатора зlibraryDependencies += "org.apache.spark" %% "spark-core" % SPARK_VERSION withSources() libraryDependencies += "org.apache.spark" %% "spark-catalyst" % SPARK_VERSION withSources()
rad i

Доброго дня, Хуан! Що ви маєте на увазі під "Не вдається вирішити тест символу"? Де це сталося?
Євген Лопаткін

13

Мені подобається створювати SparkSessionTestWrapperрису, яку можна змішувати для тестових занять. Підхід Шанкара працює, але він занадто повільний для тестових наборів із декількома файлами.

import org.apache.spark.sql.SparkSession

trait SparkSessionTestWrapper {

  lazy val spark: SparkSession = {
    SparkSession.builder().master("local").appName("spark session").getOrCreate()
  }

}

Цю рису можна використовувати наступним чином:

class DatasetSpec extends FunSpec with SparkSessionTestWrapper {

  import spark.implicits._

  describe("#count") {

    it("returns a count of all the rows in a DataFrame") {

      val sourceDF = Seq(
        ("jets"),
        ("barcelona")
      ).toDF("team")

      assert(sourceDF.count === 2)

    }

  }

}

Перевірте проект spark-spec на реальний приклад, який використовує SparkSessionTestWrapperпідхід.

Оновлення

Іскрове тестування бази бібліотеки автоматично додає SparkSession , коли деякі риси поєднуються в до тестового класу (наприклад , коли DataFrameSuiteBaseзмішуються, ви будете мати доступ до SparkSession через sparkзмінний).

Я створив окрему тестову бібліотеку під назвою spark-fast-tests, щоб дати користувачам повний контроль над SparkSession під час запуску їх тестів. Я не думаю, що тестова допоміжна бібліотека повинна встановлювати SparkSession. Користувачі повинні мати можливість запускати і зупиняти свою SparkSession, як їм зручно (мені подобається створювати одну SparkSession і використовувати її протягом усього запуску тестового набору).

Ось приклад дії assertSmallDatasetEqualityметоду швидких іскрових тестів :

import com.github.mrpowers.spark.fast.tests.DatasetComparer

class DatasetSpec extends FunSpec with SparkSessionTestWrapper with DatasetComparer {

  import spark.implicits._

    it("aliases a DataFrame") {

      val sourceDF = Seq(
        ("jose"),
        ("li"),
        ("luisa")
      ).toDF("name")

      val actualDF = sourceDF.select(col("name").alias("student"))

      val expectedDF = Seq(
        ("jose"),
        ("li"),
        ("luisa")
      ).toDF("student")

      assertSmallDatasetEquality(actualDF, expectedDF)

    }

  }

}

1
У цьому підході, як ви рекомендуєте sparkSession.stop()десь додати ?
Ніл Бест

Вам не потрібно буде sparkSession.stop()@NeilBest. Сесія Spark буде вимкнена, коли тестовий пакет завершиться.
Поуерс

1
чому не потрібно sparkSession.stop ()? як відповідь @Shankar Koirala зупинить іскрову сесію, це марно?
yuxh

@yuxh - відповідь Шанкара починається і зупиняється сеансом Spark після кожного тесту. Цей підхід працює, але він дійсно повільний, тому що для початку сеансу Spark потрібен деякий час.
Повноваження

1
але він також згадує іскрову базу, SharedSparkContext зупиняє цей контекст після всіх тестових випадків. Я не бачу зупинки коду навіть після всіх тестових випадків у вашому SparkSessionTestWrapper
yuxh

1

Я міг би вирішити проблему за допомогою коду нижче

залежність іскрового вулика додана до проекту pom

class DataFrameTest extends FunSuite with DataFrameSuiteBase{
        test("test dataframe"){
        val sparkSession=spark
        import sparkSession.implicits._
        var df=sparkSession.read.format("csv").load("path/to/csv")
        //rest of the operations.
        }
        }

0

Ще один спосіб Unit Test за допомогою JUnit

import org.apache.spark.sql.SparkSession
import org.junit.Assert._
import org.junit.{After, Before, _}

@Test
class SessionSparkTest {
  var spark: SparkSession = _

  @Before
  def beforeFunction(): Unit = {
    //spark = SessionSpark.getSparkSession()
    spark = SparkSession.builder().appName("App Name").master("local").getOrCreate()
    System.out.println("Before Function")
  }

  @After
  def afterFunction(): Unit = {
    spark.stop()
    System.out.println("After Function")
  }

  @Test
  def testRddCount() = {
    val rdd = spark.sparkContext.parallelize(List(1, 2, 3))
    val count = rdd.count()
    assertTrue(3 == count)
  }

  @Test
  def testDfNotEmpty() = {
    val sqlContext = spark.sqlContext
    import sqlContext.implicits._
    val numDf = spark.sparkContext.parallelize(List(1, 2, 3)).toDF("nums")
    assertFalse(numDf.head(1).isEmpty)
  }

  @Test
  def testDfEmpty() = {
    val sqlContext = spark.sqlContext
    import sqlContext.implicits._
    val emptyDf = spark.sqlContext.createDataset(spark.sparkContext.emptyRDD[Num])
    assertTrue(emptyDf.head(1).isEmpty)
  }
}

case class Num(id: Int)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.