четверг, 16 ноября 2023 г.

Unit и Integration testing

Unit testing

Требования к коду и тестам

  • Тест должен быть как можно более быстрым
  • Тестируемая функция должна быть детерминистической, т.е. зависеть только от входных параметров
  • Тесты можно запускать как самостоятельно, так и автоматически при git push
  • Нужно стремиться к 100% покрытия кода тестами
    Покрытие = число if в коде * 2 / число тестов
    *2 , т.к. должна быть протестирована как положительная, так и отрицательная часть if

Обеспечение детерминированности функции

Самый распространенный способ - это мокирование ( mock / stub / spy )
Замена реальных зависимостей на заглушки, иметирующих поведение.

Реализация

Python

Зависимые библиотеки
import unittest
from unittest import mock
from unittest.mock import patch
Объявление заглушки:
@mock.patch('что заменяем', side_effect=на что)
Мок функция получает все параметры исходной:
def mocked_subprocess_run(*args, **kwargs):
    return Subprocess_Ret(kwargs.get('args', None))

def mocked_os_listdir(*args, **kwargs):
    return []
Пример теста
class Tests_01_AwsHook(unittest.TestCase):
    def setUp(self):
        self.hook = AwsHook(end_point="https://pointtest.com", bucket_name="buckettest")
        
    @mock.patch('subprocess.run', side_effect=mocked_subprocess_run)
    @mock.patch('os.listdir', side_effect=mocked_os_listdir)
    def test_05_upload_folder_content(self, mock_subprocess, mock_os_listdir):
        ret = self.hook.upload_folder_content("local_test1", "aws_test2", need_clear = False)
        ret_calc = 'aws --endpoint-url=https://pointtest.com s3 cp --recursive --no-progress "local_test1/" "s3://buckettest/aws_test2/"'
        self.assertEqual(ret == ret_calc, True)

Scala

Языки на основании JVM не дают возможности мокирования приватных частей кода.
Не получится сделать @mock.patch, как в python, для любой функции
Внешние зависимости должны передаваться параметрами, т.е. код функции должен быть иммутабелен и детерменестичен, только в этом случае функция будет тестируемая

Пример
Плохой нетестируемый код
class A {
  private val b = new B()

  def doSomething() {
    b.someMethod()
  }
}
Хороший код: внешняя зависимость передается через dependency injection в параметрах класса
class A(b: B = new B) {
  def doSomething(): Unit = {
    b.someMethod()
  }
}
Тогда в тесте эту внешнюю зависимость B просто замокировать
val b = mock[B]
//Side effect
when(b.someMethod()).thenReturn(xx)

val classToTest = new A(b)
classToTest.doSomething()

Integration testing

Цель

в противоположность изолированных Unit тестов, интеграционное тестировани - это тестирование ПО в связке с зависимыми системами.

Типы зависимостей

Управляемые - это часть системы, где работает тестируемое приложение.
К примеру, это локальная база конкретного приложения.
Но, в таком случае, тестирование предполагает проведение интеграционного теста в тестовой среде.

Неуправляемые - все за пределами приложения.
Сюда же можно отнести тестирование на проде, т.к. мы не можем вносить изменения во внутренние прод бд, она для нас неуправляемая зависимость.
Если бд используют несколько приложений, то это тоже нельзя считать управляемой зависимостью.

Методика интеграционного теста

  • интеграционный тест предполагает проверку как можно длинной цепочки событий
  • дополнительно тестируются крайние случаи
  • как следствие, число интеграционных тестов должно быть минимально, но они должны покрывать весь функционал приложения

Обработка неуправляемых зависимостей

  • Заглушки - описано в Unit тестировании.
    Не очень подходит для интеграционного тестирования, т.к. трудно эмулировать работу целой системы через заглушки.
  • Тест контейнеры - docker контейнер с заменой неуправляемой зависимости

Реализация

Первичные требования - в системе должен быть установлен Docker

Python

Описание и исходные коды

В системе должны быть пакеты (используется внутри самого приложения testcontainers):
pip install sqlalchemy
pip install psycopg2
Устанавливаем нужный тест контейнер, например, postgres
pip install testcontainers[postgres]
Загрузка и запуск контейнера
from testcontainers.postgres import PostgresContainer
import sqlalchemy
import psycopg2

#Вытаскиваем и запускаем через API контейнер postgres:9.5
postgres = PostgresContainer("postgres:9.5")
postgres.start()
#Pulling image postgres:9.5
#
#Container started: 0ccf062553
#Waiting to be ready...
Заполнение тестовый контейнер данными
engine = sqlalchemy.create_engine(postgres.get_connection_url())
engine.execute("create table test(id INT, c text)")
engine.execute("insert into test values(1, 'test')")
Выполняем интеграционный тест
engine.execute("select * from test").fetchone()
#(1, 'test')
Чистим тестовый контейнер
postgres.stop()
Явные start/stop могут быть заменены на блок with:
with PostgresContainer("postgres:9.5") as postgres:

Scala

Описание и исходные коды

Зависимости в build.sbt
scalaVersion := "2.11.12"

libraryDependencies ++= Seq(
  "com.dimafeng" %% "testcontainers-scala-scalatest" % "0.40.16",
   "com.dimafeng" %% "testcontainers-scala-postgresql" % "0.40.16",
   "org.postgresql" % "postgresql" % "42.6.0",
   "org.scalatest" %% "scalatest-flatspec" % "3.2.16"
)
Описание контейнера и его запуск
import com.dimafeng.testcontainers.PostgreSQLContainer
import com.dimafeng.testcontainers.scalatest.TestContainerForAll
import org.testcontainers.utility.DockerImageName
import java.sql.DriverManager

val containerDef = PostgreSQLContainer.Def(
    dockerImageName = DockerImageName.parse("postgres:15.1"),
    databaseName = "testcontainer-scala",
    username = "scala",
    password = "scala"
  )

val containerObj = containerDef.start()
Заполнение тестовый контейнер данными
Class.forName(containerObj.driverClassName)
val connection = DriverManager.getConnection(containerObj.jdbcUrl, containerObj.username, containerObj.password)

connection.prepareStatement("create table TEST_SCALA1(id INT, col TEXT)").execute
connection.prepareStatement("insert into TEST_SCALA1 values(1, 'test')").execute
Выполняем интеграционный тест
val query = connection.prepareStatement("select * from TEST_SCALA1").executeQuery
if(query.next) {
    ( query.getInt(1), query.getString(2))
    //res23: (Int, String) = (1,test)
}
Чистим тестовый контейнер
query.close()
connection.close()
containerObj.stop()

Комментариев нет:

Отправить комментарий