Интеграционное тестирование микросервисов на Scala
О себе
- Программирую с 2011 года
- PHP
- JavaScript
- Java
- Scala
- 2GIS
- Реклама
- BigData
- Crawler
Casino
- 25+ сервисов на Scala
- PostgreSQL
- Kafka
- ZeroMQ
- Cassandra
- Hadoop
- Spark
- Machine learning stuff
- ...
Как тестировать?
Unit tests
Integration tests
CI env
CI env
deps
APP
push
config
jar
run
test
+ Это работает
- CI-окружение всегда включено — лишняя трата ресурсов
- Все зависимости также всегда включены
- Невозможно запустить тесты двух веток одновременно
- Очень тяжело настроить локально
- Невозможно тестировать старт/стоп/рестарт
Integration tests
+ Легко запускать локально
Gitlab CI + docker
CI env
CI env
deps
APP
push
docker
run
test
Принципиально не отличается от прошлой схемы, но появляется docker
docker-compose
docker-compose
deps
APP
push
run
test
docker-compose
+/- Проще настроить локально, но всё еще нужно согласовывать несколько мест
+ Не нужно держать отдельное окружение и зависимости
+ Можно тестировать разные ветки одновременно
- Всё еще невозможно тестировать старт/стоп/рестарт
- Сложности с запуском
Запуск docker-compose
docker-compose 2
version: '2.1'
services:
web:
build: .
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
redis:
image: redis
db:
image: db
healthcheck:
test: "some test here"
Запуск docker-compose
docker-compose 3
Запуск docker-compose
docker-compose 3
version: '3'
services:
web:
build: .
depends_on: [ db, redis ]
redis:
image: redis
command: [ "./wait-for-it.sh", ... ]
db:
image: redis
command: [ "./wait-for-db.sh", ... ]
wait-for-it.sh
Запуск docker-compose
docker-compose 3
version: '3'
services:
postgres:
...
my_service:
depends_on: [ postgres ]
...
sbt:
depends_on: [ my_service ]
...
docker-compose-run.sh
Запуск docker-compose
docker-compose 3
docker-compose up -d postgres
docker-compose-run.sh
docker-compose run sbt
down $?
docker-compose up -d my_service
wait_until 10 2 docker-compose exec -T \
my_service sh -c "netstat -ntlp | grep 80 || exit 1"
wait_until 10 2 docker-compose exec -T \
postgres psql postgresql://my_user:my_pass@postgres:5432/my_service \
-c "SELECT PostGIS_version();"
Запуск docker-compose
docker-compose 3
function down {
echo "Exiting with code $1"
if [[ $1 -eq 0 ]]; then
docker-compose down
exit $1
else
docker-compose logs -t postgres my_service
docker-compose down
exit $1
fi
}
docker-compose-run.sh
2. docker-it-scala
1. docker-java / spotify docker-client
3. Testcontainers
Testcontainers
- Java-библиотека для запуска и тестирования
docker-контейнеров
- Есть Scala-фасад testcontainers-scala
- Из коробки поддерживает ряд популярных сервисов: postgresql, mysql, nginx, kafka, selenium
- Можно запускать любые другие контейнеры
- Простое и гибкое API
Testcontainers
val pgContainer: PostgreSQLContainer =
PostgreSQLContainer("postgres:9.6")
Predefined containers
val pgUrl: String = pgContainer.jdbcUrl
val pgPort: Int = pgContainer.mappedPort(5432)
pgContainer.finished()
pgContainer.starting()
Testcontainers
class GenericContainer(
imageName: String,
exposedPorts: Seq[Int] = Seq(),
env: Map[String, String] = Map(),
command: Seq[String] = Seq(),
classpathResourceMapping: Seq[(String, String, BindMode)] = Seq(),
waitStrategy: Option[WaitStrategy] = None
) ...
Custom containers
Testcontainers
class PostgresqlSpec extends FlatSpec
with ForAllTestContainer {
Running tests
override val container = PostgreSQLContainer()
"PostgreSQL container" should "be started" in {
Class.forName(container.driverClassName)
val connection = DriverManager
.getConnection(container.jdbcUrl, container.username, container.password)
// test some stuff
}
}
Testcontainers
val pgContainer = PostgreSQLContainer()
val myContainer = MyContainer()
override val container =
MultipleContainers(pgContainer, myContainer)
Running tests
Testcontainers
deps
APP
push
run
test
Testcontainers
+ Есть гибкие healthcheck'и
+ Наконец, можно тестировать старт/стоп/рестарт
+ Никаких *.sh-файликов в репозитории
+ Можно конфигурировать приложение и тесты как угодно гибко
+ Одинаково легко запускается на CI и локально
+ Не нужно держать отдельное окружение и зависимости
+ Можно тестировать разные ветки одновременно
Dependent containers
Exception encountered when invoking run on a nested suite - Mapped port can only be obtained after the container is started
class MySpec extends FlatSpec
with ForAllTestContainer {
val pgCont = PostgreSQLContainer()
override val container = MultipleContainers(appCont, pgCont)
val appCont =
AppContainer(pgCont.jdbcUrl, pgCont.username, pgCont.password)
// tests here
}
class MyTest extends FreeSpec
with BeforeAndAfterAll {
lazy val pgCont = PostgreSQLContainer()
lazy val appCont =
AppContainer(pgCont.jdbcUrl, pgCont.username, pgCont.password)
override def beforeAll(): Unit = ??? // start all containers
override def afterAll(): Unit = ??? // shutdown all containers
// tests here
}
org.postgresql.util.PSQLException: Connection to localhost:32787 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.
@Override
public String getJdbcUrl() {
return "jdbc:postgresql://" +
getContainerIpAddress() +
":" +
getMappedPort(POSTGRESQL_PORT) +
"/" +
databaseName;
}
Рекомендация разработчиков testcontainers: создавать руками network для взаимодействия между контейнерами
class MyTest extends FreeSpec with BeforeAndAfterAll {
val network: Network = Network.newNetwork()
val dbName = "some_db"
val pgContainerAlias = "postgres"
val jdbcUrl = s"jdbc:postgresql://$pgContainerAlias:5432/$dbName"
lazy val pgCont = {
val c = PostgreSQLContainer("postgres:9.6")
c.container.withNetwork(network)
c.container.withNetworkAliases(pgContainerAlias)
c.container.withDatabaseName(dbName)
c
}
lazy val appCont = {
val c = AppContainer(jdbcUrl, pgCont.username, pgCont.password)
c.container.withNetwork(network)
c
}
override def beforeAll(): Unit = ???
override def afterAll(): Unit = ??? // shutdown containers + network
// tests here
}
class MyTest extends FreeSpec
with ForAllTestContainer
with BeforeAndAfterAll {
val network: Network = Network.newNetwork()
val dbName = "some_db"
val pgContainerAlias = "postgres"
val jdbcUrl = s"jdbc:postgresql://$pgContainerAlias:5432/$dbName"
lazy val pgCont = {
val c = PostgreSQLContainer("postgres:9.6")
c.container.withNetwork(network)
c.container.withNetworkAliases(pgContainerAlias)
c.container.withDatabaseName(dbName)
c
}
lazy val appCont = {
val c = AppContainer(jdbcUrl, pgCont.username, pgCont.password)
c.container.withNetwork(network)
c
}
override val container = MultipleContainers(pgCont, appCont)
override def afterAll(): Unit = ??? // shutdown network
// tests here
}
Mocks
val hostIp = ???
AppContainer(sparkJobServerMockHost = hostIp)
val sparkJobServerMock = new SparkJobServerMock()
sparkJobServerMock.init(someData)
val apiResult = appApi.callMethod()
assert(apiResult == someData)
val sparkJobServerMock = new SparkJobServerMock()
val sparkJobServerMock = new SparkJobServerMock()
sparkJobServerMock.init(someData)
val sparkJobServerMock = new SparkJobServerMock()
sparkJobServerMock.init(someData)
val apiResult = appApi.callMethod()
val sparkJobServerMock = new SparkJobServerMock()
sparkJobServerMock.init(someData)
val apiResult = appApi.callMethod()
assert(apiResult == someData)
Mocks
val client: com.github.dockerjava.api.DockerClient =
DockerClientFactory
.instance
.client
Работает не везде!
val networkInfo: com.github.dockerjava.api.model.Network =
client
.inspectNetworkCmd()
.withNetworkId(network.getId)
.exec()
val hostIp: String =
networkInfo
.getIpam
.getConfig
.get(0)
.getGateway
https://github.com/rnorth/containercore
Итоги
- Testcontainers — очень хорошая либа, которая решает наши проблемы
- Сделать нормальные интеграционные тесты сложно
- Докер помогает, но тянет за собой кучу "особенностей"
- Но текущее API добавляет немного боли
Спасибо!
lmnet89@gmail.com
Бадальянц Юрий, 2018
Интеграционное тестирование микросервисов на Scala
By Yury Badalyants
Интеграционное тестирование микросервисов на Scala
Вариант презентации для РИТ++ 2018
- 1,416