web-dev-qa-db-ja.com

Spark 2.0+で単体テストを作成する方法

私は、JUnitテストフレームワークでSparkSessionをテストするための合理的な方法を見つけようとしています。 SparkContextには良い例があるように見えますが、 spark-testing-base の内部のいくつかの場所で使用されているにもかかわらず、SparkSessionに対応する例を動作させる方法を理解できませんでした。ここに行くのが本当に正しい方法でない場合は、spark-testing-baseも使用しないソリューションを試してみてください。

シンプルなテストケース( 完全なMWEプロジェクト with 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では、 より適切なエラーがスローされます

52
bbarker

このすばらしい質問を世に出してくれてありがとう。何らかの理由で、Sparkに関しては、誰もが分析に夢中になり、過去15年ほどで出現した優れたソフトウェアエンジニアリングの実践を忘れています。これが、コースでテストと継続的統合(特にDevOpsなど)を議論することを重要視する理由です。

用語の簡単な説明

true単体テストは、テスト内のすべてのコンポーネントを完全に制御できることを意味します。データベース、REST呼び出し、ファイルシステム、またはシステムクロックとの相互作用はありません。 Gerard Mezarosが xUnit Test Patterns に記述しているように、すべてを「二重化」する必要があります(例:モック、スタブなど)。これはセマンティクスのように見えますが、本当に重要です。これを理解していないことが、継続的インテグレーションで断続的なテストエラーが発生する大きな理由の1つです。

単体テストが可能

このため、RDDの単体テストは不可能です。ただし、分析を開発するときは、単体テストの場所がまだあります。

簡単な操作を検討してください。

rdd.map(foo).map(bar)

ここで、fooおよびbarは単純な関数です。これらは通常の方法で単体テストできます。また、できる限り多くのコーナーケースを使用する必要があります。結局のところ、テストフィクスチャであるかRDDであるかどうかから入力を取得する場所を気にするのはなぜですか?

Spark Shellを忘れないでください

これはper自体のテストではありませんが、これらの初期段階では、Sparkシェルで実験して、変換、特にアプローチの結果を把握する必要があります。たとえば、toDebugStringexplainglomshowprintSchemaなどのさまざまな関数を使用して、物理的および論理的なクエリプラン、パーティション分割戦略、保存、およびデータの状態を調べることができます。それらを探させます。

Sparkシェルおよびテストでlocal[2]にマスターを設定して、作業の分散を開始した後にのみ発生する可能性のある問題を特定することもできます。

Sparkによる統合テスト

楽しいものにしましょう。

ヘルパー関数とRDD/DataFrame変換ロジックの品質に自信を感じた後に統合テスト Sparkを行うには、いくつかのことを行うことが重要です(ビルドツールに関係なく)およびテストフレームワーク):

  • JVMメモリを増やします。
  • 分岐を有効にしますが、並列実行は無効にします。
  • テストフレームワークを使用して、Spark統合テストをスイートに蓄積し、すべてのテストの前にSparkContextを初期化し、すべてのテストの後に停止します。

ScalaTestでは、BeforeAndAfterAll(私が一般的に好む)またはBeforeAndAfterEachasを@ShankarKoiralaが混在させて、Sparkアーティファクトを初期化および破棄できます。私はこれが例外を作るのに合理的な場所であることを知っていますが、私はあなたが使用しなければならないそれらの可変varsが本当に好きではありません。

ローンパターン

別のアプローチは、 Loan Pattern を使用することです。

例(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()
  }
} 

ご覧のとおり、Loanパターンは高次関数を使用してSparkContextをテストに「貸し付け」、それが完了した後に破棄します。

Suffering-Oriented Programming(Thanks、Nathan)

それは完全に好みの問題ですが、別のフレームワークを導入する前にできる限り、Loanパターンを使用して自分で物事を結び付けることを好みます。フレームワークは、軽量を維持しようとするだけでなく、テストの失敗をデバッグするのが難しくなる「魔法」を追加することがあります。そこで私は Suffering-Oriented Programming アプローチを採用しました。そこでは、新しいフレームワークを追加することを、それを持たないことの苦痛が耐えるには大きすぎるまで避けています。しかし、これもあなた次第です。

その代替フレームワークの最良の選択は、もちろん spark-testing-base @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は、scSparkContextとして、すべて無料で提供してくれました。個人的には、ローンパターンが必要なことを正確に行うため、この目的のためだけにこの依存関係を持ち込むことはありません。また、分散システムでは非常に多くの予測不能性が発生するため、継続的インテグレーションで問題が発生した場合にサードパーティライブラリのソースコードで発生するマジックをトレースしなければならないのは非常に苦痛です。

spark-testing-baseが本当に輝くのは、HDFSClusterLikeYARNClusterLikeのようなHadoopベースのヘルパーです。これらの特性を混在させることで、セットアップの苦痛を大幅に軽減できます。それが輝く別の場所は、 Scalacheck -likeプロパティとジェネレーターです-もちろん、プロパティベースのテストがどのように機能し、なぜそれが役立つかを理解していると仮定します。しかし、ここでも、分析とテストがそのレベルの洗練度に達するまで、個人的に使用を保留します。

「シスだけがアブソリュートを扱う。」 -オビ=ワン・ケノービ

もちろん、どちらかを選択する必要はありません。おそらく、ほとんどのテストでLoan Patternアプローチを使用し、spark-testing-baseをより厳密なテストでのみ使用できます。選択肢はバイナリではありません。両方を行うことができます。

Spark St​​reamingを使用した統合テスト

最後に、メモリ内の値を使用したSparkStreaming統合テストのセットアップがspark-testing-baseなしでどのように見えるかについてのスニペットを提示したいと思います。

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に送るだけです。そのほとんどは、実際にSpark AP​​Iで機能する定型的なセットアップです。とにかく、これをStreamingSuiteBaseと比較できます にあるようにspark-testing-base.

これは私の最長の投稿かもしれないので、ここに残します。他のすべてのアプリケーション開発を改善したのと同じアジャイルなソフトウェアエンジニアリング手法で、分析の品質を向上させるために、他の人が他のアイデアに耳を傾けることを願っています。

そして、恥知らずなプラグの謝罪で、あなたは私たちのコースをチェックすることができます Apache Sparkによる分析 、ここでこれらの多くのアイデアなどに対処します。オンライン版を近日中にリリースする予定です。

79
Vidya

以下のように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}

Holden Karauは本当にすてきなテストを書いています spark-testing-base

以下をチェックアウトする必要があるのは簡単な例です

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)
  }
}

お役に立てれば!

20
Shankar Koirala

私は、テストクラスに混在できるSparkSessionTestWrapper特性を作成するのが好きです。 Shankarのアプローチは機能しますが、複数のファイルを含むテストスイートでは非常に遅くなります。

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)

    }

  }

}

SparkSessionTestWrapperアプローチを使用する実際の例については、 spark-spec プロジェクトを確認してください。

更新

spark-testing-base library は、特定の特性がテストクラスに混在するときにSparkSessionを自動的に追加します(たとえば、DataFrameSuiteBaseが混在する場合、SparkSessionにアクセスするには、 spark変数)。

spark-fast-tests という別のテストライブラリを作成して、テストの実行時にユーザーがSparkSessionを完全に制御できるようにしました。テストヘルパーライブラリがSparkSessionを設定する必要はないと思います。ユーザーは、必要に応じてSparkSessionを開始および停止できる必要があります(1つのSparkSessionを作成し、テストスイートの実行全体で使用するのが好きです)。

動作中のspark-fast-tests 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)

    }

  }

}
10
Powers

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.3SharedSparkSession が利用可能であるため:

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"

さらに、さまざまなテストスーツの巨大なセットがある場合、 テストソース of Sparkを確認できます。

9
Eugene Lopatkin

私は以下のコードで問題を解決できました

spark-Hive依存関係がプロジェクト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.
        }
        }
1
sunitha

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)
0