Udostępnij za pośrednictwem


Testowanie jednostkowe notatników

Możesz użyć testów jednostkowych , aby zwiększyć jakość i spójność kodu notesów. Testowanie jednostkowe to podejście do testowania niezależnych jednostek kodu, takich jak funkcje, wcześnie i często. Pomaga to szybciej znaleźć problemy z kodem, szybciej odkrywać błędne założenia dotyczące kodu i usprawniać ogólne wysiłki związane z kodowaniem.

Ten artykuł stanowi wprowadzenie do podstawowego testowania jednostkowego przy użyciu funkcji. Zaawansowane pojęcia, takie jak klasy i interfejsy do testów jednostkowych, a także użycie stubów , mocków i harnessów testowych , choć wspierane przy testowaniu jednostkowym dla notebooków, znajdują się poza zakresem tego artykułu. Ten artykuł nie obejmuje również innych rodzajów metod testowania, takich jak testowanie integracji , testowanie systemu , testowania akceptacyjnegolub niefunkcjonalne metody testowania, takie jak testowanie wydajnościowe lub testowanie użyteczności .

W tym artykule przedstawiono następujące kwestie:

  • Jak organizować funkcje i ich testy jednostkowe.
  • Jak pisać funkcje w języku Python, R, Scala oraz funkcjach zdefiniowanych przez użytkownika w języku SQL, które są dobrze zaprojektowane do testowania jednostkowego.
  • Jak wywoływać te funkcje z notesów Python, R, Scala i SQL.
  • Jak pisać testy jednostkowe w językach Python, R i Scala przy użyciu popularnych struktur testowych pytest dla języka Python, testthat dla języka R i ScalaTest dla języka Scala. Ponadto, jak napisać kod SQL do testowania jednostkowego funkcji zdefiniowanych przez użytkownika (UDF) w SQL.
  • Jak uruchamiać te testy jednostkowe w notesach Python, R, Scala i SQL.

Notatka

Usługa Azure Databricks zaleca pisanie i uruchamianie testów jednostkowych w notesie. Chociaż niektóre polecenia można uruchomić w terminalu internetowym, terminal internetowy ma więcej ograniczeń, takich jak brak obsługi platformy Spark. Zobacz Wykonywanie poleceń powłoki w terminalu internetowym usługi Azure Databricks.

Organizowanie funkcji i testów jednostkowych

Istnieje kilka typowych metod organizowania funkcji i testów jednostkowych za pomocą notesów. Każde podejście ma swoje zalety i wyzwania.

W przypadku notesów Python, R i Scala typowe podejścia obejmują następujące rozwiązania:

  • Przechowuj funkcje i ich testy jednostkowe poza notatnikami..
    • Korzyści: te funkcje można wywoływać za pomocą notesów i spoza nich. Frameworki testowe są lepiej przystosowane do uruchamiania testów poza notesami.
    • Wyzwania: Takie podejście nie jest obsługiwane dla notesów Scala. Takie podejście zwiększa również liczbę plików do śledzenia i konserwacji.
  • Przechowuj funkcje w jednym notatniku i ich testy jednostkowe w osobnym notatniku..
    • Korzyści: te funkcje są łatwiejsze do ponownego użycia w notesach.
    • Wyzwania: liczba notesów do śledzenia i konserwacji zwiększa się. Tych funkcji nie można używać poza notesami. Te funkcje mogą być również trudniejsze do testowania poza notesami.
  • Przechowuj funkcje i ich testy jednostkowe w tym samym notatniku..
    • Korzyści: funkcje i ich testy jednostkowe są przechowywane w jednym notesie w celu łatwiejszego śledzenia i konserwacji.
    • Wyzwania: Te funkcje mogą być trudniejsze do ponownego użycia w notatnikach. Tych funkcji nie można używać poza notesami. Te funkcje mogą być również trudniejsze do testowania poza notesami.

W przypadku notesów języka Python i R usługa Databricks zaleca przechowywanie funkcji i testów jednostkowych poza notesami. W przypadku notesów Scala usługa Databricks zaleca umieszczenie funkcji w jednym notesie i testów jednostkowych w osobnym notesie.

W przypadku notesów SQL usługa Databricks zaleca przechowywanie funkcji jako funkcji zdefiniowanych przez użytkownika SQL (UDF) w schematach (nazywanych również bazami danych). Następnie można wywołać te funkcje UDF SQL oraz testy jednostkowe tych funkcji z notesów SQL.

Funkcje zapisu

W tej sekcji opisano prosty zestaw przykładowych funkcji, które określają następujące elementy:

  • Czy tabela istnieje w bazie danych.
  • Czy kolumna istnieje w tabeli.
  • Ile wierszy w kolumnie odpowiada wartości w tej kolumnie.

Te funkcje mają być proste, dzięki czemu można skupić się na szczegółach testowania jednostkowego w tym artykule, a nie skoncentrować się na samych funkcjach.

Aby uzyskać najlepsze wyniki testów jednostkowych, funkcja powinna zwrócić pojedynczy przewidywalny wynik i mieć jeden typ danych. Na przykład aby sprawdzić, czy coś istnieje, funkcja powinna zwrócić wartość logiczną true lub false. Aby zwrócić liczbę istniejących wierszy, funkcja powinna zwrócić nieujemną liczbę całkowitą. W pierwszym przykładzie nie powinien zwracać wartości false, jeśli coś nie istnieje, ani samej rzeczy, jeśli istnieje. Podobnie w drugim przykładzie, program nie powinien zwracać ani liczby istniejących wierszy, ani wartości false, gdy nie ma żadnych wierszy.

Te funkcje można dodać do istniejącego obszaru roboczego usługi Azure Databricks w następujący sposób w językach Python, R, Scala lub SQL.

Pyton

Poniższy kod zakłada, że masz skonfigurowane foldery Git usługi Databricks, dodano repozytorium i otwarto repozytorium w obszarze roboczym usługi Azure Databricks.

Utwórz plik o nazwie myfunctions.py w repozytorium i dodaj następującą zawartość do pliku. Inne przykłady w tym artykule oczekują, że ten plik zostanie nazwany myfunctions.py. Możesz użyć różnych nazw dla własnych plików.

import pyspark
from pyspark.sql import SparkSession
from pyspark.sql.functions import col

# Because this file is not a Databricks notebook, you
# must create a Spark session. Databricks notebooks
# create a Spark session for you by default.
spark = SparkSession.builder \
                    .appName('integrity-tests') \
                    .getOrCreate()

# Does the specified table exist in the specified database?
def tableExists(tableName, dbName):
  return spark.catalog.tableExists(f"{dbName}.{tableName}")

# Does the specified column exist in the given DataFrame?
def columnExists(dataFrame, columnName):
  if columnName in dataFrame.columns:
    return True
  else:
    return False

# How many rows are there for the specified value in the specified column
# in the given DataFrame?
def numRowsInColumnForValue(dataFrame, columnName, columnValue):
  df = dataFrame.filter(col(columnName) == columnValue)

  return df.count()

R

Poniższy kod zakłada, że masz skonfigurowane foldery Git usługi Databricks, dodano repozytorium i otwarto repozytorium w obszarze roboczym usługi Azure Databricks.

Utwórz plik o nazwie myfunctions.r w repozytorium i dodaj następującą zawartość do pliku. Inne przykłady w tym artykule oczekują, że ten plik zostanie nazwany myfunctions.r. Możesz użyć różnych nazw dla własnych plików.

library(SparkR)

# Does the specified table exist in the specified database?
table_exists <- function(table_name, db_name) {
  tableExists(paste(db_name, ".", table_name, sep = ""))
}

# Does the specified column exist in the given DataFrame?
column_exists <- function(dataframe, column_name) {
  column_name %in% colnames(dataframe)
}

# How many rows are there for the specified value in the specified column
# in the given DataFrame?
num_rows_in_column_for_value <- function(dataframe, column_name, column_value) {
  df = filter(dataframe, dataframe[[column_name]] == column_value)

  count(df)
}

Skala

Utwórz notes Scala o nazwie myfunctions z następującą zawartością. Inne przykłady w tym artykule zakładają, że ten notatnik nazywa się myfunctions. Możesz użyć różnych nazw dla własnych notesów.

import org.apache.spark.sql.DataFrame
import org.apache.spark.sql.functions.col

// Does the specified table exist in the specified database?
def tableExists(tableName: String, dbName: String) : Boolean = {
  return spark.catalog.tableExists(dbName + "." + tableName)
}

// Does the specified column exist in the given DataFrame?
def columnExists(dataFrame: DataFrame, columnName: String) : Boolean = {
  val nameOfColumn = null

  for(nameOfColumn <- dataFrame.columns) {
    if (nameOfColumn == columnName) {
      return true
    }
  }

  return false
}

// How many rows are there for the specified value in the specified column
// in the given DataFrame?
def numRowsInColumnForValue(dataFrame: DataFrame, columnName: String, columnValue: String) : Long = {
  val df = dataFrame.filter(col(columnName) === columnValue)

  return df.count()
}

SQL

Poniższy kod zakłada, że masz przykładowy zestaw danych innej firmy diamenty w schemacie o nazwie default w katalogu o nazwie main, który jest dostępny z obszaru roboczego usługi Azure Databricks. Jeśli katalog lub schemat, którego chcesz użyć, ma inną nazwę, zmień jedną lub obie poniższe instrukcje USE, aby pasować.

Utwórz notatnik SQL i dodaj następującą zawartość do tego nowego notatnika. Następnie dołączyć notatnik do klastra i uruchomić notatnik, aby dodać następujące funkcje zdefiniowane przez użytkownika SQL do określonego katalogu i schematu.

Notatka

Funkcje zdefiniowane przez użytkownika SQL table_exists i column_exists działają tylko z Unity Catalog. Obsługa funkcji UDF SQL dla katalogu Unity jest dostępna w publicznej wersji zapoznawczej.

USE CATALOG main;
USE SCHEMA default;

CREATE OR REPLACE FUNCTION table_exists(catalog_name STRING,
                                        db_name      STRING,
                                        table_name   STRING)
  RETURNS BOOLEAN
  RETURN if(
    (SELECT count(*) FROM system.information_schema.tables
     WHERE table_catalog = table_exists.catalog_name
       AND table_schema  = table_exists.db_name
       AND table_name    = table_exists.table_name) > 0,
    true,
    false
  );

CREATE OR REPLACE FUNCTION column_exists(catalog_name STRING,
                                         db_name      STRING,
                                         table_name   STRING,
                                         column_name  STRING)
  RETURNS BOOLEAN
  RETURN if(
    (SELECT count(*) FROM system.information_schema.columns
     WHERE table_catalog = column_exists.catalog_name
       AND table_schema  = column_exists.db_name
       AND table_name    = column_exists.table_name
       AND column_name   = column_exists.column_name) > 0,
    true,
    false
  );

CREATE OR REPLACE FUNCTION num_rows_for_clarity_in_diamonds(clarity_value STRING)
  RETURNS BIGINT
  RETURN SELECT count(*)
         FROM main.default.diamonds
         WHERE clarity = clarity_value

Funkcje wywoływania

W tej sekcji opisano kod, który wywołuje poprzednie funkcje. Można na przykład użyć tych funkcji, aby zliczyć liczbę wierszy w tabeli, w której istnieje określona wartość w określonej kolumnie. Jednak przed kontynuowaniem należy sprawdzić, czy tabela rzeczywiście istnieje i czy kolumna rzeczywiście istnieje w tej tabeli. Poniższy kod sprawdza te warunki.

Jeśli funkcje z poprzedniej sekcji zostały dodane do obszaru roboczego usługi Azure Databricks, możesz wywołać te funkcje z obszaru roboczego w następujący sposób.

Pyton

Utwórz notes języka Python w tym samym folderze co poprzedni plik myfunctions.py w repozytorium i dodaj następującą zawartość do notesu. Zmień wartości zmiennych dla nazwy tabeli, nazwy schematu (bazy danych), nazwy kolumny i wartości kolumny zgodnie z potrzebami. Następnie dołączyć notatnik do klastra i uruchomić notatnik, aby wyświetlić wyniki.

from myfunctions import *

tableName   = "diamonds"
dbName      = "default"
columnName  = "clarity"
columnValue = "VVS2"

# If the table exists in the specified database...
if tableExists(tableName, dbName):

  df = spark.sql(f"SELECT * FROM {dbName}.{tableName}")

  # And the specified column exists in that table...
  if columnExists(df, columnName):
    # Then report the number of rows for the specified value in that column.
    numRows = numRowsInColumnForValue(df, columnName, columnValue)

    print(f"There are {numRows} rows in '{tableName}' where '{columnName}' equals '{columnValue}'.")
  else:
    print(f"Column '{columnName}' does not exist in table '{tableName}' in schema (database) '{dbName}'.")
else:
  print(f"Table '{tableName}' does not exist in schema (database) '{dbName}'.") 

R

Utwórz notes języka R w tym samym folderze co poprzedni plik myfunctions.r w repozytorium i dodaj następującą zawartość do notesu. Zmień wartości zmiennych dla nazwy tabeli, nazwy schematu (bazy danych), nazwy kolumny i wartości kolumny zgodnie z potrzebami. Następnie dołączyć notatnik do klastra i uruchomić notatnik, aby wyświetlić wyniki.

library(SparkR)
source("myfunctions.r")

table_name   <- "diamonds"
db_name      <- "default"
column_name  <- "clarity"
column_value <- "VVS2"

# If the table exists in the specified database...
if (table_exists(table_name, db_name)) {

  df = sql(paste("SELECT * FROM ", db_name, ".", table_name, sep = ""))

  # And the specified column exists in that table...
  if (column_exists(df, column_name)) {
    # Then report the number of rows for the specified value in that column.
    num_rows = num_rows_in_column_for_value(df, column_name, column_value)

    print(paste("There are ", num_rows, " rows in table '", table_name, "' where '", column_name, "' equals '", column_value, "'.", sep = "")) 
  } else {
    print(paste("Column '", column_name, "' does not exist in table '", table_name, "' in schema (database) '", db_name, "'.", sep = ""))
  }

} else {
  print(paste("Table '", table_name, "' does not exist in schema (database) '", db_name, "'.", sep = ""))
}

Skala

Utwórz nowy notes Scala w tym samym folderze co poprzedniego notesu Scala myfunctions i dodaj następującą zawartość do tego nowego notesu.

W pierwszej komórce tego nowego notesu dodaj poniższy kod, który wywoła magię%run. Ta magia sprawia, że zawartość notesu myfunctions jest dostępna dla Twojego nowego notesu.

%run ./myfunctions

W drugiej komórce tego nowego notesu dodaj następujący kod. Zmień wartości zmiennych dla nazwy tabeli, nazwy schematu (bazy danych), nazwy kolumny i wartości kolumny zgodnie z potrzebami. Następnie dołączyć notatnik do klastra i uruchomić notatnik, aby wyświetlić wyniki.

val tableName   = "diamonds"
val dbName      = "default"
val columnName  = "clarity"
val columnValue = "VVS2"

// If the table exists in the specified database...
if (tableExists(tableName, dbName)) {

  val df = spark.sql("SELECT * FROM " + dbName + "." + tableName)

  // And the specified column exists in that table...
  if (columnExists(df, columnName)) {
    // Then report the number of rows for the specified value in that column.
    val numRows = numRowsInColumnForValue(df, columnName, columnValue)

    println("There are " + numRows + " rows in '" + tableName + "' where '" + columnName + "' equals '" + columnValue + "'.")
  } else {
    println("Column '" + columnName + "' does not exist in table '" + tableName + "' in database '" + dbName + "'.")
  }

} else {
  println("Table '" + tableName + "' does not exist in database '" + dbName + "'.")
}

SQL

Dodaj następujący kod do nowej komórki w poprzednim notesie lub do komórki w osobnym notesie. W razie potrzeby zmień nazwy schematu lub katalogu, aby pasować do Twoich, a następnie uruchom tę komórkę, aby wyświetlić wyniki.

SELECT CASE
-- If the table exists in the specified catalog and schema...
WHEN
  table_exists("main", "default", "diamonds")
THEN
  -- And the specified column exists in that table...
  (SELECT CASE
   WHEN
     column_exists("main", "default", "diamonds", "clarity")
   THEN
     -- Then report the number of rows for the specified value in that column.
     printf("There are %d rows in table 'main.default.diamonds' where 'clarity' equals 'VVS2'.",
            num_rows_for_clarity_in_diamonds("VVS2"))
   ELSE
     printf("Column 'clarity' does not exist in table 'main.default.diamonds'.")
   END)
ELSE
  printf("Table 'main.default.diamonds' does not exist.")
END

Pisanie testów jednostkowych

W tej sekcji opisano kod, który testuje poszczególne funkcje opisane na początku tego artykułu. Jeśli w przyszłości wprowadzisz jakiekolwiek zmiany w funkcjach, możesz użyć testów jednostkowych, aby określić, czy te funkcje nadal działają zgodnie z oczekiwaniami.

Jeśli funkcje zostały dodane na początku tego artykułu do obszaru roboczego usługi Azure Databricks, możesz dodać testy jednostkowe dla tych funkcji do obszaru roboczego w następujący sposób.

Pyton

Utwórz inny plik o nazwie test_myfunctions.py w tym samym folderze co poprzedni plik myfunctions.py w repozytorium i dodaj następującą zawartość do pliku. Domyślnie pytest wyszukuje pliki .py, których nazwy zaczynają się od test_ (lub kończą się _test) do przetestowania. Podobnie domyślnie pytest szuka wewnątrz tych plików funkcji, których nazwy zaczynają się od test_, aby przetestować.

Ogólnie rzecz biorąc, najlepszą praktyką jest nie uruchamiać testów jednostkowych względem funkcji pracujących z danymi w środowisku produkcyjnym. Jest to szczególnie ważne w przypadku funkcji, które dodają, usuńą lub w inny sposób zmieniają dane. Aby chronić dane produkcyjne przed zagrożeniem przez testy jednostkowe w nieoczekiwany sposób, należy uruchomić testy jednostkowe na danych nieprodukcyjnych. Jednym z typowych podejść jest tworzenie fałszywych danych, które są jak najbardziej zbliżone do danych produkcyjnych. Poniższy przykład kodu tworzy fałszywe dane dla testów jednostkowych.

import pytest
import pyspark
from myfunctions import *
from pyspark.sql import SparkSession
from pyspark.sql.types import StructType, StructField, IntegerType, FloatType, StringType

tableName    = "diamonds"
dbName       = "default"
columnName   = "clarity"
columnValue  = "SI2"

# Because this file is not a Databricks notebook, you
# must create a Spark session. Databricks notebooks
# create a Spark session for you by default.
spark = SparkSession.builder \
                    .appName('integrity-tests') \
                    .getOrCreate()

# Create fake data for the unit tests to run against.
# In general, it is a best practice to not run unit tests
# against functions that work with data in production.
schema = StructType([ \
  StructField("_c0",     IntegerType(), True), \
  StructField("carat",   FloatType(),   True), \
  StructField("cut",     StringType(),  True), \
  StructField("color",   StringType(),  True), \
  StructField("clarity", StringType(),  True), \
  StructField("depth",   FloatType(),   True), \
  StructField("table",   IntegerType(), True), \
  StructField("price",   IntegerType(), True), \
  StructField("x",       FloatType(),   True), \
  StructField("y",       FloatType(),   True), \
  StructField("z",       FloatType(),   True), \
])

data = [ (1, 0.23, "Ideal",   "E", "SI2", 61.5, 55, 326, 3.95, 3.98, 2.43 ), \
         (2, 0.21, "Premium", "E", "SI1", 59.8, 61, 326, 3.89, 3.84, 2.31 ) ]

df = spark.createDataFrame(data, schema)

# Does the table exist?
def test_tableExists():
  assert tableExists(tableName, dbName) is True

# Does the column exist?
def test_columnExists():
  assert columnExists(df, columnName) is True

# Is there at least one row for the value in the specified column?
def test_numRowsInColumnForValue():
  assert numRowsInColumnForValue(df, columnName, columnValue) > 0

R

Utwórz inny plik o nazwie test_myfunctions.r w tym samym folderze co poprzedni plik myfunctions.r w repozytorium i dodaj następującą zawartość do pliku. Domyślnie testthat wyszukuje pliki .r, których nazwy zaczynają się od test do testowania.

Ogólnie rzecz biorąc, najlepszą praktyką jest nie uruchamiać testów jednostkowych względem funkcji pracujących z danymi w środowisku produkcyjnym. Jest to szczególnie ważne w przypadku funkcji, które dodają, usuńą lub w inny sposób zmieniają dane. Aby chronić dane produkcyjne przed zagrożeniem przez testy jednostkowe w nieoczekiwany sposób, należy uruchomić testy jednostkowe na danych nieprodukcyjnych. Jednym z typowych podejść jest tworzenie fałszywych danych, które są jak najbardziej zbliżone do danych produkcyjnych. Poniższy przykład kodu tworzy fałszywe dane dla testów jednostkowych.

library(testthat)
source("myfunctions.r")

table_name   <- "diamonds"
db_name      <- "default"
column_name  <- "clarity"
column_value <- "SI2"

# Create fake data for the unit tests to run against.
# In general, it is a best practice to not run unit tests
# against functions that work with data in production.
schema <- structType(
  structField("_c0",     "integer"),
  structField("carat",   "float"),
  structField("cut",     "string"),
  structField("color",   "string"),
  structField("clarity", "string"),
  structField("depth",   "float"),
  structField("table",   "integer"),
  structField("price",   "integer"),
  structField("x",       "float"),
  structField("y",       "float"),
  structField("z",       "float"))

data <- list(list(as.integer(1), 0.23, "Ideal",   "E", "SI2", 61.5, as.integer(55), as.integer(326), 3.95, 3.98, 2.43),
             list(as.integer(2), 0.21, "Premium", "E", "SI1", 59.8, as.integer(61), as.integer(326), 3.89, 3.84, 2.31))

df <- createDataFrame(data, schema)

# Does the table exist?
test_that ("The table exists.", {
  expect_true(table_exists(table_name, db_name))
})

# Does the column exist?
test_that ("The column exists in the table.", {
  expect_true(column_exists(df, column_name))
})

# Is there at least one row for the value in the specified column?
test_that ("There is at least one row in the query result.", {
  expect_true(num_rows_in_column_for_value(df, column_name, column_value) > 0)
})

Skala

Utwórz nowy notes Scala w tym samym folderze co poprzedniego notesu Scala myfunctions i dodaj następującą zawartość do tego nowego notesu.

W pierwszej komórce nowego notesu dodaj następujący kod, który wywołuje magię %run . Ta magia sprawia, że zawartość notesu myfunctions jest dostępna dla Twojego nowego notesu.

%run ./myfunctions

W drugiej komórce dodaj następujący kod. Ten kod definiuje testy jednostkowe i określa sposób ich uruchamiania.

Ogólnie rzecz biorąc, najlepszą praktyką jest nie uruchamiać testów jednostkowych względem funkcji pracujących z danymi w środowisku produkcyjnym. Jest to szczególnie ważne w przypadku funkcji, które dodają, usuńą lub w inny sposób zmieniają dane. Aby chronić dane produkcyjne przed zagrożeniem przez testy jednostkowe w nieoczekiwany sposób, należy uruchomić testy jednostkowe na danych nieprodukcyjnych. Jednym z typowych podejść jest tworzenie fałszywych danych, które są jak najbardziej zbliżone do danych produkcyjnych. Poniższy przykład kodu tworzy fałszywe dane dla testów jednostkowych.

import org.scalatest._
import org.apache.spark.sql.types.{StructType, StructField, IntegerType, FloatType, StringType}
import scala.collection.JavaConverters._

class DataTests extends AsyncFunSuite {

  val tableName   = "diamonds"
  val dbName      = "default"
  val columnName  = "clarity"
  val columnValue = "SI2"

  // Create fake data for the unit tests to run against.
  // In general, it is a best practice to not run unit tests
  // against functions that work with data in production.
  val schema = StructType(Array(
                 StructField("_c0",     IntegerType),
                 StructField("carat",   FloatType),
                 StructField("cut",     StringType),
                 StructField("color",   StringType),
                 StructField("clarity", StringType),
                 StructField("depth",   FloatType),
                 StructField("table",   IntegerType),
                 StructField("price",   IntegerType),
                 StructField("x",       FloatType),
                 StructField("y",       FloatType),
                 StructField("z",       FloatType)
               ))

  val data = Seq(
                  Row(1, 0.23, "Ideal",   "E", "SI2", 61.5, 55, 326, 3.95, 3.98, 2.43),
                  Row(2, 0.21, "Premium", "E", "SI1", 59.8, 61, 326, 3.89, 3.84, 2.31)
                ).asJava

  val df = spark.createDataFrame(data, schema)

  // Does the table exist?
  test("The table exists") {
    assert(tableExists(tableName, dbName) == true)
  }

  // Does the column exist?
  test("The column exists") {
    assert(columnExists(df, columnName) == true)
  }

  // Is there at least one row for the value in the specified column?
  test("There is at least one matching row") {
    assert(numRowsInColumnForValue(df, columnName, columnValue) > 0)
  }
}

nocolor.nodurations.nostacks.stats.run(new DataTests)

Notatka

W tym przykładzie kodu jest używany styl FunSuite testowania w języku ScalaTest. Aby uzyskać informacje o innych dostępnych stylach testowania, zobacz Wybieranie stylów testowania dla projektu.

SQL

Przed dodaniem testów jednostkowych należy pamiętać, że ogólnie rzecz biorąc, najlepszą praktyką jest nie uruchamianie testów jednostkowych dla funkcji, które współpracują z danymi w środowisku produkcyjnym. Jest to szczególnie ważne w przypadku funkcji, które dodają, usuńą lub w inny sposób zmieniają dane. Aby chronić dane produkcyjne przed zagrożeniem przez testy jednostkowe w nieoczekiwany sposób, należy uruchomić testy jednostkowe na danych nieprodukcyjnych. Typowym podejściem jest uruchamianie testów jednostkowych na widokach zamiast tabel.

Aby utworzyć widok, możesz wywołać polecenie CREATE VIEW z nowej komórki w poprzednim notesie lub osobnym notesie. W poniższym przykładzie przyjęto założenie, że masz istniejącą tabelę o nazwie diamonds w schemacie (bazie danych) o nazwie default w katalogu o nazwie main. Zmień te nazwy, aby pasowały do Twoich potrzeb, a następnie uruchom tylko tę komórkę.

USE CATALOG main;
USE SCHEMA default;

CREATE VIEW view_diamonds AS
SELECT * FROM diamonds;

Po utworzeniu widoku dodaj każdą z poniższych instrukcji SELECT do nowej komórki w poprzednim notatniku lub do nowej komórki w osobnym notatniku. Zmień nazwy, aby dopasować je do własnych, zgodnie z potrzebami.

SELECT if(table_exists("main", "default", "view_diamonds"),
          printf("PASS: The table 'main.default.view_diamonds' exists."),
          printf("FAIL: The table 'main.default.view_diamonds' does not exist."));

SELECT if(column_exists("main", "default", "view_diamonds", "clarity"),
          printf("PASS: The column 'clarity' exists in the table 'main.default.view_diamonds'."),
          printf("FAIL: The column 'clarity' does not exists in the table 'main.default.view_diamonds'."));

SELECT if(num_rows_for_clarity_in_diamonds("VVS2") > 0,
          printf("PASS: The table 'main.default.view_diamonds' has at least one row where the column 'clarity' equals 'VVS2'."),
          printf("FAIL: The table 'main.default.view_diamonds' does not have at least one row where the column 'clarity' equals 'VVS2'."));

Uruchamianie testów jednostkowych

W tej sekcji opisano sposób uruchamiania testów jednostkowych zakodowanych w poprzedniej sekcji. Po uruchomieniu testów jednostkowych uzyskasz wyniki pokazujące, które testy jednostkowe przeszły i zakończyły się niepowodzeniem.

Jeśli testy jednostkowe zostały dodane z poprzedniej sekcji do obszaru roboczego usługi Azure Databricks, możesz uruchomić te testy jednostkowe z obszaru roboczego. Możesz uruchomić te testy jednostkowe ręcznie lub zgodnie z harmonogramem.

Pyton

Utwórz notes języka Python w tym samym folderze co poprzedni plik test_myfunctions.py w repozytorium i dodaj następującą zawartość.

W pierwszej komórce nowego notesu dodaj następujący kod, a następnie uruchom komórkę, która wywołuje magię %pip . To magiczne zaklęcie instaluje pytest.

%pip install pytest

W drugiej komórce dodaj następujący kod, a następnie uruchom komórkę. Wyniki pokazują, które testy jednostkowe przeszły i zakończyły się niepowodzeniem.

import pytest
import sys

# Skip writing pyc files on a readonly filesystem.
sys.dont_write_bytecode = True

# Run pytest.
retcode = pytest.main([".", "-v", "-p", "no:cacheprovider"])

# Fail the cell execution if there are any test failures.
assert retcode == 0, "The pytest invocation failed. See the log for details."

R

Utwórz notes języka R w tym samym folderze co poprzedni plik test_myfunctions.r w repozytorium i dodaj następującą zawartość.

W pierwszej komórce dodaj następujący kod, a następnie uruchom komórkę, która wywołuje funkcję install.packages. Ta funkcja instaluje testthat.

install.packages("testthat")

W drugiej komórce dodaj następujący kod, a następnie uruchom komórkę. Wyniki pokazują, które testy jednostkowe przeszły i zakończyły się niepowodzeniem.

library(testthat)
source("myfunctions.r")

test_dir(".", reporter = "tap")

Skala

Uruchom pierwsze, a następnie drugie komórki w notesie z poprzedniej sekcji. Wyniki pokazują, które testy jednostkowe przeszły i zakończyły się niepowodzeniem.

SQL

Uruchom każdą z trzech komórek w notatniku z poprzedniej sekcji. Wyniki pokazują, czy każdy test jednostkowy zakończył się powodzeniem, czy niepowodzeniem.

Jeśli nie potrzebujesz już widoku po uruchomieniu testów jednostkowych, możesz usunąć widok. Aby usunąć ten widok, możesz dodać następujący kod do nowej komórki w jednym z poprzednich notesów, a następnie uruchomić tylko tę komórkę.

DROP VIEW view_diamonds;

Napiwek

Wyniki uruchomień notatnika (w tym wyniki testów jednostkowych) można wyświetlić w dziennikach sterowników klastra. Możesz również określić lokalizację przesyłania logów klastra.

Możesz skonfigurować system ciągłej integracji i ciągłego dostarczania lub ciągłego wdrażania (CI/CD), taki jak GitHub Actions, aby automatycznie uruchamiać testy jednostkowe za każdym razem, gdy kod ulegnie zmianie. Aby zapoznać się z przykładem, zobacz omówienie GitHub Actions w Software engineering best practices for notebooks.

Dodatkowe zasoby

pytest

testthat

ScalaTest

SQL