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 контейнер с заменой неуправляемой зависимости
Реализация
Первичные требования - в системе должен быть установлен DockerPython
Описание и исходные кодыВ системе должны быть пакеты (используется внутри самого приложения 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()
Комментариев нет:
Отправить комментарий