私は、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では、 より適切なエラーがスローされます 。
このすばらしい質問を世に出してくれてありがとう。何らかの理由で、Sparkに関しては、誰もが分析に夢中になり、過去15年ほどで出現した優れたソフトウェアエンジニアリングの実践を忘れています。これが、コースでテストと継続的統合(特にDevOpsなど)を議論することを重要視する理由です。
用語の簡単な説明
true単体テストは、テスト内のすべてのコンポーネントを完全に制御できることを意味します。データベース、REST呼び出し、ファイルシステム、またはシステムクロックとの相互作用はありません。 Gerard Mezarosが xUnit Test Patterns に記述しているように、すべてを「二重化」する必要があります(例:モック、スタブなど)。これはセマンティクスのように見えますが、本当に重要です。これを理解していないことが、継続的インテグレーションで断続的なテストエラーが発生する大きな理由の1つです。
単体テストが可能
このため、RDD
の単体テストは不可能です。ただし、分析を開発するときは、単体テストの場所がまだあります。
簡単な操作を検討してください。
rdd.map(foo).map(bar)
ここで、foo
およびbar
は単純な関数です。これらは通常の方法で単体テストできます。また、できる限り多くのコーナーケースを使用する必要があります。結局のところ、テストフィクスチャであるかRDD
であるかどうかから入力を取得する場所を気にするのはなぜですか?
Spark Shellを忘れないでください
これはper自体のテストではありませんが、これらの初期段階では、Sparkシェルで実験して、変換、特にアプローチの結果を把握する必要があります。たとえば、toDebugString
、explain
、glom
、show
、printSchema
などのさまざまな関数を使用して、物理的および論理的なクエリプラン、パーティション分割戦略、保存、およびデータの状態を調べることができます。それらを探させます。
Sparkシェルおよびテストでlocal[2]
にマスターを設定して、作業の分散を開始した後にのみ発生する可能性のある問題を特定することもできます。
Sparkによる統合テスト
楽しいものにしましょう。
ヘルパー関数とRDD
/DataFrame
変換ロジックの品質に自信を感じた後に統合テスト Sparkを行うには、いくつかのことを行うことが重要です(ビルドツールに関係なく)およびテストフレームワーク):
SparkContext
を初期化し、すべてのテストの後に停止します。ScalaTestでは、BeforeAndAfterAll
(私が一般的に好む)またはBeforeAndAfterEach
asを@ShankarKoiralaが混在させて、Sparkアーティファクトを初期化および破棄できます。私はこれが例外を作るのに合理的な場所であることを知っていますが、私はあなたが使用しなければならないそれらの可変var
sが本当に好きではありません。
ローンパターン
別のアプローチは、 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
は、sc
をSparkContext
として、すべて無料で提供してくれました。個人的には、ローンパターンが必要なことを正確に行うため、この目的のためだけにこの依存関係を持ち込むことはありません。また、分散システムでは非常に多くの予測不能性が発生するため、継続的インテグレーションで問題が発生した場合にサードパーティライブラリのソースコードで発生するマジックをトレースしなければならないのは非常に苦痛です。
spark-testing-baseが本当に輝くのは、HDFSClusterLike
やYARNClusterLike
のようなHadoopベースのヘルパーです。これらの特性を混在させることで、セットアップの苦痛を大幅に軽減できます。それが輝く別の場所は、 Scalacheck -likeプロパティとジェネレーターです-もちろん、プロパティベースのテストがどのように機能し、なぜそれが役立つかを理解していると仮定します。しかし、ここでも、分析とテストがそのレベルの洗練度に達するまで、個人的に使用を保留します。
「シスだけがアブソリュートを扱う。」 -オビ=ワン・ケノービ
もちろん、どちらかを選択する必要はありません。おそらく、ほとんどのテストでLoan Patternアプローチを使用し、spark-testing-baseをより厳密なテストでのみ使用できます。選択肢はバイナリではありません。両方を行うことができます。
Spark Streamingを使用した統合テスト
最後に、メモリ内の値を使用した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 APIで機能する定型的なセットアップです。とにかく、これをStreamingSuiteBase
と比較できます にあるようにspark-testing-base.
これは私の最長の投稿かもしれないので、ここに残します。他のすべてのアプリケーション開発を改善したのと同じアジャイルなソフトウェアエンジニアリング手法で、分析の品質を向上させるために、他の人が他のアイデアに耳を傾けることを願っています。
そして、恥知らずなプラグの謝罪で、あなたは私たちのコースをチェックすることができます Apache Sparkによる分析 、ここでこれらの多くのアイデアなどに対処します。オンライン版を近日中にリリースする予定です。
以下のように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)
}
}
お役に立てれば!
私は、テストクラスに混在できる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)
}
}
}
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を確認できます。
私は以下のコードで問題を解決できました
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.
}
}
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)