Статьи

Тестирование насыщенного данными кода с помощью Go, часть 3

Это третья часть из пяти в серии руководств по тестированию насыщенного данными кода с помощью Go. Во второй части я рассмотрел тестирование на реальном слое данных в памяти на основе популярного SQLite. В этом руководстве я проведу тестирование на локальном сложном слое данных, который включает в себя реляционную БД и кэш Redis.

Тестирование на уровне данных в памяти — это круто. Тесты молниеносны, и у вас есть полный контроль. Но иногда вам нужно быть ближе к фактической конфигурации вашего уровня производственных данных. Вот несколько возможных причин:

  • Вы используете конкретные детали своей реляционной БД, которые хотите протестировать.
  • Ваш слой данных состоит из нескольких взаимодействующих хранилищ данных.
  • Тестируемый код состоит из нескольких процессов, обращающихся к одному и тому же уровню данных.
  • Вы хотите подготовить или наблюдать ваши тестовые данные, используя стандартные инструменты.
  • Вы не хотите реализовывать выделенный слой данных в памяти, если ваш уровень данных постоянно меняется.
  • Вы просто хотите знать, что вы тестируете на своем реальном уровне данных.
  • Вам нужно протестировать с большим количеством данных, которые не помещаются в памяти.

Я уверен, что есть и другие причины, но вы можете понять, почему во многих случаях недостаточно использовать слой данных в памяти для тестирования.

OK. Итак, мы хотим проверить фактический уровень данных. Но мы все еще хотим быть максимально легкими и проворными. Это означает локальный уровень данных. Вот преимущества:

  • Не нужно ничего настраивать и настраивать в центре обработки данных или в облаке.
  • Не нужно беспокоиться о том, что наши тесты случайно испортили производственные данные.
  • Не нужно координировать свои действия с коллегами-разработчиками в общей тестовой среде.
  • Нет медлительности по сетевым звонкам.
  • Полный контроль над содержимым слоя данных, с возможностью начать с нуля в любое время.

В этом уроке мы поднимем ставку. Мы реализуем (очень частично) гибридный уровень данных, который состоит из реляционной БД MariaDB и сервера Redis. Затем мы будем использовать Docker для поддержки локального уровня данных, который мы можем использовать в наших тестах.

Во-первых, вам нужен Докер, конечно. Проверьте документацию, если вы не знакомы с Docker. Следующим шагом является получение изображений для наших хранилищ данных: MariaDB и Redis . Не вдаваясь в подробности, MariaDB — это отличная реляционная БД, совместимая с MySQL, а Redis — отличное хранилище значений ключей в памяти (и многое другое).

01
02
03
04
05
06
07
08
09
10
> docker pull mariadb
 
> docker pull redis
 
> docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mariadb latest 51d6a5e69fa7 2 weeks ago 402MB
redis latest b6dddb991dfa 2 weeks ago 107MB

Теперь, когда у нас установлен Docker и у нас есть образы для MariaDB и Redis, мы можем написать файл docker-compose.yml, который мы будем использовать для запуска наших хранилищ данных. Давайте назовем нашу БД «songify».

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
mariadb-songify:
  image: mariadb:latest
  command: >
      —general-log
      —general-log-file=/var/log/mysql/query.log
  expose:
    — «3306»
  ports:
    — «3306:3306»
  environment:
    MYSQL_DATABASE: «songify»
    MYSQL_ALLOW_EMPTY_PASSWORD: «true»
  volumes_from:
    — mariadb-data
mariadb-data:
  image: mariadb:latest
  volumes:
    — /var/lib/mysql
  entrypoint: /bin/bash
 
redis:
  image: redis
  expose:
    — «6379»
  ports:
    — «6379:6379»

Вы можете запустить свои хранилища данных с помощью команды docker-compose up (аналогично vagrant up ). Вывод должен выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
> docker-compose up
Starting hybridtest_redis_1 …
Starting hybridtest_mariadb-data_1 …
Starting hybridtest_redis_1
Starting hybridtest_mariadb-data_1 … done
Starting hybridtest_mariadb-songify_1 …
Starting hybridtest_mariadb-songify_1 … done
Attaching to hybridtest_mariadb-data_1,
             hybridtest_redis_1,
             hybridtest_mariadb-songify_1
.
.
.
redis_1 |
redis_1 |
.
.
.
mariadb-songify_1 |
.
.
.

На этом этапе у вас есть полноценный сервер MariaDB, прослушивающий порт 3306, и сервер Redis, прослушивающий порт 6379 (оба являются стандартными портами).

Давайте воспользуемся преимуществами этих мощных хранилищ данных и обновим наш слой данных до гибридного слоя данных, который кэширует песни на пользователя в Redis. Когда GetSongsByUser() При вызове слоя данных сначала проверяется, сохраняет ли Redis песни для пользователя. Если это произойдет, то просто верните песни из Redis, но если этого не произойдет (потеря кэша), тогда он извлечет песни из MariaDB и заполнит кеш Redis, так что он готов к следующему разу.

Вот определение структуры и конструктора. Структура сохраняет дескриптор БД, как и раньше, а также клиент Redis. Конструктор подключается как к реляционной БД, так и к Redis. Он создает схему и сбрасывает redis, только если соответствующие параметры имеют значение true, что необходимо только для тестирования. В производственной среде вы создаете схему один раз (без учета миграции схемы).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type HybridDataLayer struct {
    db *sql.DB
    redis *redis.Client
}
 
func NewHybridDataLayer(dbHost string,
                        dbPort int,
                        redisHost string,
                        createSchema bool,
                        clearRedis bool) (*HybridDataLayer,
                                          error) {
    dsn := fmt.Sprintf(«root@tcp(%s:%d)/», dbHost, dbPort)
    if createSchema {
        err := createMariaDBSchema(dsn)
        if err != nil {
            return nil, err
        }
    }
 
    db, err := sql.Open(«mysql»,
                         dsn+»desongcious?parseTime=true»)
    if err != nil {
        return nil, err
    }
 
    redisClient := redis.NewClient(&redis.Options{
        Addr: redisHost + «:6379»,
        Password: «»,
        DB: 0,
    })
 
    _, err = redisClient.Ping().Result()
    if err != nil {
        return nil, err
    }
 
    if clearRedis {
        redisClient.FlushDB()
    }
 
    return &HybridDataLayer{db, redisClient}, nil
}

MariaDB и SQLite немного отличаются в том, что касается DDL. Различия небольшие, но важные. Go не имеет зрелого инструментария для работы с несколькими БД, как, например, фантастическая SQLAlchemy в Python, поэтому вам придется управлять им самостоятельно (нет, Gorm не в счет). Основными отличиями являются:

  • Драйвер SQL является «github.com/go-sql-driver/mysql».
  • База данных не хранится в памяти, поэтому она каждый раз воссоздается (удаляется и создается).
  • Схема должна быть частью независимых операторов DDL вместо одной строки всех операторов.
  • Автоинкрементные первичные ключи отмечены как AUTO_INCREMENT .
  • VARCHAR вместо TEXT .

Вот код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
func createMariaDBSchema(dsn string) error {
    db, err := sql.Open(«mysql», dsn)
    if err != nil {
        return err
    }
 
    // Recreate DB
    commands := []string{
        «DROP DATABASE songify;»,
        «CREATE DATABASE songify;»,
    }
    for _, s := range (commands) {
        _, err = db.Exec(s)
        if err != nil {
            return err
        }
    }
 
    // Create schema
    db, err = sql.Open(«mysql», dsn+»songify?parseTime=true»)
    if err != nil {
        return err
    }
 
    schema := []string{
        `CREATE TABLE IF NOT EXISTS song (
          id INTEGER PRIMARY KEY AUTO_INCREMENT,
          url VARCHAR(2088) UNIQUE,
          title VARCHAR(100),
          description VARCHAR(500)
        );`,
        `CREATE TABLE IF NOT EXISTS user (
          id INTEGER PRIMARY KEY AUTO_INCREMENT,
          name VARCHAR(100),
          email VARCHAR(100) UNIQUE,
          registered_at TIMESTAMP,
          last_login TIMESTAMP
        );`,
        «CREATE INDEX user_email_idx ON user (email);»,
        `CREATE TABLE IF NOT EXISTS label (
          id INTEGER PRIMARY KEY AUTO_INCREMENT,
          name VARCHAR(100) UNIQUE
        );`,
        «CREATE INDEX label_name_idx ON label (name);»,
        `CREATE TABLE IF NOT EXISTS label_song (
          label_id INTEGER NOT NULL REFERENCES label (id),
          song_id INTEGER NOT NULL REFERENCES song (id),
          PRIMARY KEY (label_id, song_id)
        );`,
        `CREATE TABLE IF NOT EXISTS user_song (
          user_id INTEGER NOT NULL REFERENCES user (id),
          song_id INTEGER NOT NULL REFERENCES song (id),
          PRIMARY KEY (user_id, song_id)
        );`,
    }
 
    for _, s := range (schema) {
        _, err = db.Exec(s)
        if err != nil {
            return err
        }
    }
    return nil
}

Redis очень прост в использовании от Go. Клиентская библиотека github.com/go-redis/redis очень интуитивна и точно выполняет команды Redis. Например, чтобы проверить, существует ли ключ, вы просто используете метод Exits() клиента redis, который принимает один или несколько ключей и возвращает их количество.

В этом случае я проверяю только один ключ:

1
2
3
4
count, err := m.redis.Exists(email).Result()
   if err != nil {
       return err
   }

Тесты на самом деле идентичны. Интерфейс не изменился, и поведение не изменилось. Единственное изменение заключается в том, что реализация теперь хранит кэш в Redis. Метод GetSongsByEmail() теперь просто вызывает refreshUser_Redis() .

1
2
3
4
5
func (m *HybridDataLayer) GetSongsByUser(u User) (songs []Song,
                                                  err error) {
    err = m.refreshUser_Redis(u.Email, &songs)
    return
}

Метод refreshUser_Redis() возвращает пользовательские песни из Redis, если они существуют, и в противном случае извлекает их из MariaDB.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
type Songs *[]Song
 
func (m *HybridDataLayer) refreshUser_Redis(email string,
                                            out Songs) error {
    count, err := m.redis.Exists(email).Result()
    if err != nil {
        return err
    }
 
    if count == 0 {
        err = m.getSongsByUser_DB(email, out)
        if err != nil {
            return err
        }
 
        for _, song := range *out {
            s, err := serializeSong(song)
            if err != nil {
                return err
            }
 
            _, err = m.redis.SAdd(email, s).Result()
            if err != nil {
                return err
            }
        }
        return
    }
 
    members, err := m.redis.SMembers(email).Result()
    for _, member := range members {
        song, err := deserializeSong([]byte(member))
        if err != nil {
            return err
        }
        *out = append(*out, song)
    }
 
    return out, nil
}

Здесь есть небольшая проблема с точки зрения методологии тестирования. Когда мы тестируем через интерфейс абстрактного уровня данных, мы не видим реализации уровня данных.

Например, возможно, что существует большой недостаток, когда слой данных полностью пропускает кэш и всегда извлекает данные из БД. Тесты пройдут, но мы не сможем извлечь выгоду из кеша. В пятой части я расскажу о тестировании вашего кеша, что очень важно.

В этом руководстве мы рассмотрели тестирование локального сложного слоя данных, состоящего из нескольких хранилищ данных (реляционная БД и кэш Redis). Мы также использовали Docker для простого развертывания нескольких хранилищ данных для тестирования.

В четвертой части мы сконцентрируемся на тестировании с использованием удаленных хранилищ данных, используя моментальные снимки производственных данных для наших тестов, а также на создании собственных тестовых данных. Будьте на связи!