Udostępnij za pośrednictwem


Testowanie jednostkowe notesó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 samodzielnych jednostek kodu, takich jak funkcje, wczesne 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 za pomocą funkcji. Zaawansowane pojęcia, takie jak klasy testów jednostkowych i interfejsy, a także użycie wycinków, makietów i uprzęży testowych, a także obsługiwane podczas testowania jednostkowego dla notesów, wykraczają poza zakres tego artykułu. Ten artykuł nie obejmuje również innych rodzajów metod testowania, takich jak testowanie integracji, testowanie systemu, testowanie akceptacyjne lub 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 for R i ScalaTest for Scala. Ponadto jak napisać język SQL, który testuje funkcje zdefiniowane przez użytkownika SQL (funkcje zdefiniowane przez użytkownika SQL).
  • Jak uruchamiać te testy jednostkowe z notesów Python, R, Scala i SQL.

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:

  • Przechowywanie funkcji i testów jednostkowych poza notesami.
    • Korzyści: te funkcje można wywoływać za pomocą notesów i spoza nich. Struktury testowe są lepiej zaprojektowane do uruchamiania testów poza notesami.
    • Wyzwania: Takie podejście nie jest obsługiwane w przypadku notesów Języka Scala. Takie podejście zwiększa również liczbę plików do śledzenia i konserwacji.
  • Przechowywanie funkcji w jednym notesie i ich testach jednostkowych w osobnym notesie.
    • 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 notesie.
    • 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 notesach. 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 Języka Scala usługa Databricks zaleca uwzględnienie 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 zdefiniowane przez użytkownika SQL i ich testy jednostkowe 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 istnieje w kolumnie dla 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, lub sama rzecz, jeśli istnieje. Podobnie w drugim przykładzie nie powinna zwracać liczby wierszy, które istnieją lub false, jeśli nie istnieją żadne wiersze.

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.

Python

Poniższy kod zakłada, że masz skonfigurowane foldery Git usługi Databricks (Repos), 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 będzie miał nazwę 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 (Repos), 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 będzie miał nazwę 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)
}

Scala

Utwórz notes Scala o nazwie z myfunctions następującą zawartością. Inne przykłady w tym artykule oczekują, że ten notes będzie miał nazwę 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ładowe diamenty zestawu danych innej firmy w schemacie o nazwie default katalogu, 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 USE instrukcje, aby pasować.

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

Uwaga

Funkcje zdefiniowane przez użytkownika table_exists SQL i column_exists działają tylko z wykazem aparatu Unity. Obsługa funkcji zdefiniowanej przez użytkownika SQL dla wykazu aparatu 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.

Python

Utwórz notes języka Python w tym samym folderze co poprzedni myfunctions.py plik 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łącz notes do klastra i uruchom notes, 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 myfunctions.r plik 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łącz notes do klastra i uruchom notes, 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 = ""))
}

Scala

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

W pierwszej komórce tego nowego notesu dodaj następujący kod, który wywołuje magię %run . Ta magia sprawia, że zawartość notesu myfunctions jest dostępna dla 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łącz notes do klastra i uruchom notes, 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.

Python

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

Ogólnie rzecz biorąc, najlepszym rozwiązaniem jest nie uruchamianie testów jednostkowych na funkcjach, 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 naruszeniem zabezpieczeń przez testy jednostkowe w nieoczekiwany sposób, należy uruchomić testy jednostkowe przed danymi nieprodukcyjnymi. 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 do uruchomienia.

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 myfunctions.r plik w repozytorium i dodaj następującą zawartość do pliku. Domyślnie wyszukuje .r pliki, testthat których nazwy zaczynają się od test do testowania.

Ogólnie rzecz biorąc, najlepszym rozwiązaniem jest nie uruchamianie testów jednostkowych na funkcjach, 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 naruszeniem zabezpieczeń przez testy jednostkowe w nieoczekiwany sposób, należy uruchomić testy jednostkowe przed danymi nieprodukcyjnymi. 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 do uruchomienia.

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

Scala

Utwórz kolejny notes Scala w tym samym folderze co poprzedni myfunctions notes Scala 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 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, najlepszym rozwiązaniem jest nie uruchamianie testów jednostkowych na funkcjach, 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 naruszeniem zabezpieczeń przez testy jednostkowe w nieoczekiwany sposób, należy uruchomić testy jednostkowe przed danymi nieprodukcyjnymi. 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 do uruchomienia.

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)

Uwaga

W tym przykładzie kodu używany jest FunSuite styl 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, najlepszym rozwiązaniem jest nie uruchamianie testów jednostkowych względem 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 naruszeniem zabezpieczeń przez testy jednostkowe w nieoczekiwany sposób, należy uruchomić testy jednostkowe przed danymi nieprodukcyjnymi. Jedną z typowych metod jest uruchamianie testów jednostkowych względem widoków 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 w schemacie (bazie danych) o nazwie diamonds default w katalogu o nazwie main. Zmień te nazwy tak, aby były zgodne z własnymi potrzebami, a następnie uruchom tylko te komórki.

USE CATALOG main;
USE SCHEMA default;

CREATE VIEW view_diamonds AS
SELECT * FROM diamonds;

Po utworzeniu widoku dodaj każdą z poniższych SELECT instrukcji do własnej nowej komórki w poprzednim notesie lub do własnej nowej komórki w osobnym notesie. 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. Te testy jednostkowe można uruchamiać ręcznie lub zgodnie z harmonogramem.

Python

Utwórz notes języka Python w tym samym folderze co poprzedni test_myfunctions.py plik 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 . Ta magia instaluje program 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 test_myfunctions.r plik 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 install.packages funkcję. Ta funkcja instaluje program 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")

Scala

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 notesie 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 przebiegów notesu (w tym wyników testu jednostkowego) można wyświetlić w dziennikach sterowników klastra. Możesz również określić lokalizację dostarczania dziennika 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 pokrycie funkcji GitHub Actions w artykule Software engineering best practices for notebooks (Najlepsze rozwiązania dotyczące inżynierii oprogramowania dla notesów).

Dodatkowe zasoby

pytest

testthat

ScalaTest

SQL