Les tests avec Spark : sortir la tête de l'eau

Vous galérez à faire des tests avec Spark ? Vous en avez marre de déclarer une Spark session à chaque fois ? Nous avons la solution pour vous ! Avec Spark-Test™, dites adieu à la saleté faites vos tests sans difficultés !

Eh bien, en voila une question intéressante ! Spark-Test est l'une des nombreuses bibliothèques que contient Spark-Tools, un ensemble d'outils visant à simplifier la vie des utilisateurs de Spark. Ne vous inquiétez pas, vous aurez l'occasion de découvrir les autres outils au fil de nos articles sur ce blog (avec déjà un article sur Spark-ZIO et des articles prémisses à Plumbus ici et ).

À la découverte de Spark-Test

Aujourd'hui, on commence par la bibliothèque qui est la plus facile à prendre en main. Et oui, vous l'aurez compris on va enfin se concentrer sur Spark-Test. Cette bibliothèque fournit des outils qui facilitent l'édition de tests touchant aux dataframes et/ou datasets en les rendant plus simples tout en fournissant des informations claires et précises grâce aux métadonnées!

Spark-Test est un descendant de Spark-Fast-Tests à travers lequel nous avons voulu présenter notre propre vision du problème et proposons ainsi une conception totalement différente. Cette nouvelle conception nous permet, par exemple, d'améliorer le rapport d'erreur. De plus, un chantier est en cours pour améliorer les performances et accélérer fortement les tests.

Le setup de cet outil ? Rien de compliqué. Rajouter la dépendance de Spark-Test dans votre build.sbt, puis un zeste de extends SparkTest et voilà on est parti ! Pour ceux du fond qui viennent d'arriver ne vous inquiétez pas. Voici un squelette potentiel pour débuter avec Spark-Test.


// pour scala 2.11 et 2.12
libraryDependencies += "io.univalence" %% "spark-test" % "0.3"
package io.univalence.sparktest

import org.scalatest.FunSuite

class GettingStartedTest extends FunSuite with SparkTest {

  test("some test") {
    // Commençons nos tests ici...
  }

}

Maintenant que tout est prêt, commençons par créer un dataframe qui va nous servir d'exemple tout au long de cet article. Spark-Test donne des outils qui aident à créer un dataframe ou un dataset rapidement pour nos tests. Alors, on va pas se gêner pour les utiliser !

Pas besoin de SparkSession. Tout est déjà mis en place pour vous.

val df = dataframe("{a:1, b:true}", "{a:2, b:false}")
/*
| a |   b   |
+---+-------+
| 1 | true  |
| 2 | false |
*/

Comparons deux dataframes pour le fun

Maintenant, imaginons que nous voulons comparer deux dataframes. Il faut savoir que deux types de différences peuvent apparaître : une différence dans le schéma et des valeurs différentes.

Dans un premier temps, nous allons implémenter une solution sans utiliser Spark-Test. Pour se faire, nous allons avoir besoin de récupérer le schéma des deux dataframes, récupérer les valeurs, puis les comparer tout ce petit monde pour savoir si oui ou non nos dataframes sont différents. Voici l'une des nombreuses solutions possibles.

//sans utiliser Spark-Test
def isEqual(df1: Dataframe, df2: Dataframe): Unit = {
  if (!df1.schema.equals(df2.schema))
    throw new Exception("Les schemas sont différents")
  if (!df1.collect().sameElements(df2.collect()))
    throw new Exception("Les valeurs sont différentes")
}

test("some test without Spark-Test") {
  val df1 = dataframe("{a:1}")
  val df2 = dataframe("{a:1, c:false}")

  isEqual(df1, df2)
  /*
   * java.lang.Exception:
   * Les schemas sont différents
   */
}

Cette solution comporte plusieurs problèmes :

  • Le manque d'informations fournies par notre Exception : on peut savoir si le problème d'égalité vient du schéma ou s'il vient des valeurs. Mais on ne peut pas savoir quel est exactement le problème, à savoir en quoi le schéma est différent ou quelles valeurs sont différentes.
  • Le manque de flexibilité : on peut par exemple ne vouloir comparer que les colonnes communes entre les deux dataframes, par exemple ici uniquement la colonne "a".

Cela peut sembler anodin dans le cas présent, puisque nos dataframes ne contiennent pas plus de deux colonnes et deux lignes. Mais imaginez deux dataframes avec des milliers de colonnes et des millions de lignes...

Maintenant, résolvons ce problème à l'aide de Spark-Test.

test("some test with Spark-Test") {
  val df1 = dataframe("{a:1}")
  val df2 = dataframe("{a:1, c:false}")

  df1.assertEquals(df2)
  /*
   * io.univalence.sparktest.SparkTest$SchemaError: 
   * Field c was not in the original DataFrame.
   */
}

On obtient une superbe erreur de type SchemaError. Mais ce n'est pas tout ! On a aussi le pourquoi du comment et c'est là tout l’intérêt de Spark-Test. L'erreur vient ici de la colonne 'c' qui n'est pas présente dans le dataframe d'origine.

Mais, mais... Je voulais comparer les colonnes en commun moiiii T_T

Pas de panique ! Il suffit d'utiliser la configuration de Spark-Test et de préciser que l'on veut ignorer les colonnes en trop situées dans df2.

test("some test with custom configuration") {
  val df1 = dataframe("{a:1}")
  val df2 = dataframe("{a:1, c:false}")

  withConfiguration(failOnMissingExpectedCol = false)({ df1.assertEquals(df2) })
  /*
   * Success
   */
}

Voilà, le test passe sans accroc :)

Maintenant, penchons-nous dans le cas ou deux dataframes sont différents, tout en ayant des schémas similaires. Cela s'explique par une ou plusieurs erreurs de valeur, c'est-à-dire des dissonances au sein même des colonnes.

test("some test with same schema") {
  val df1 = dataframe("{a:1, b:true}",  "{a:2, b:true}", "{a:3, b:true}")
  val df2 = dataframe("{a:1, b:false}", "{a:2, b:true}", "{a:4, b:false}")

  df1.assertEquals(df2)
  /*
   * io.univalence.sparktest.SparkTest$ValueError: 
   * The data set content is different :
   *
   * in value at b, false was not equal to true
   * dataframe({b: true, a: 1})
   * dataframe({b: false, a: 1})
   *
   * in value at b, false was not equal to true
   * in value at a, 4 was not equal to 3
   * dataframe({b: true, a: 3})
   * dataframe({b: false, a: 4})
   */
}

La voilà enfin la deuxième erreur. La fameuse ValueError. Cette erreur nous renseigne sur les différences rencontrées entre les deux dataframes en précisant l'erreur, ainsi que les lignes où se trouvent les dites erreurs que ce soit dans df1 ou dans df2.

Cette précision dans les erreurs que l'on va rencontrer, la flexibilité et la facilité d'utilisation, c'est ça la force de Spark-Test. Cette bibliothèque fournit d'autres fonctionnalités qui peuvent faciliter votre vie de data engineer. De plus, Spark-Test est Open Source. Il est disponible ici : https://github.com/univalence/spark-tools/tree/master/spark-test.