Всем привет! Меня зовут Найля, и я инженер по обеспечению качества в Т-Банке на одном из внутренних сервисов. Занимаюсь ручным и автоматизированным тестированием на проекте. Расскажу о том, как мы написали API-тесты с использованием фейков, что это такое и когда стоит отдать предпочтение им, а не мокам.

Долгое время у нас были только юнит-тесты, написанные разработчиками, и небольшое количество UI-тестов с использованием внутреннего фреймворка компании. На другие виды тестов на тот момент было недостаточно компетенций. Но время шло, и все больше всплывала потребность менять автоматизацию тестирования на проекте.

Пару слов о продукте для большего контекста

Наш продукт — это API, предназначенный для внутренних команд компании с целью повышения их продуктивности. Пользователи интегрируют API на свои веб-страницы в виде скрипта и настраивают сами. У продукта нет фронтовой составляющей, для тестирования создана специальная тестовая страница, где подключен сервис. Стек проекта — JavaScript/TypeScript. Используется микросервисная архитектура — примерно 10 микросервисов. Основная работа микросервисов связана с базами данных и внешними сервисами, поэтому ключевые проверки заключаются в корректности работы этих взаимодействий.

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

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

Вскоре у нас в команде произошли большие перемены, в ходе которых пришлось устроить марафон переписывания всего проекта на NestJS в сжатые сроки. Мы начали писать API-тесты на Jest, так как у разработчиков была экспертность по нему, а также нашлось решение для тестирования HTTP/WS-методов. В API-тестах продолжали использоваться моки для всех внешних зависимостей: баз данных, сторонних сервисов и взаимодействий между микросервисами.

Почему не стоит перебарщивать с мокированием

Моки — это подставные объекты, которые используются в тестировании для имитации поведения реальных объектов. Они могут быть настроены на возврат определенных значений и проверку вызовов методов для тестирования различных сценариев и состояний системы.

Преимущества мокирования, которые могут стать решающими:

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

  • Обычно моки легче и проще, чем реальные объекты, и тесты выполняются значительно быстрее. Это важно в проектах с большим количеством тестов, где время выполнения тестов может быть критическим фактором.

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

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

Мокирование, несмотря на свои преимущества, имеет ряд существенных ограничений. Одной из ключевых проблем является то, что чрезмерное использование моков может привести к созданию тестов, которые не отражают реальное поведение системы. Такие тесты могут пропускать баги, которые проявляются только в реальной среде.

Моки часто содержат упрощенные или не соответствующие настоящим реализации поведения зависимостей, это снижает точность тестов и доверие к их результатам. Тесты с моками можно назвать точными только при условии, что моки ведут себя так же, как настоящие реализации. А постоянную поддержку моков сложно гарантировать, учитывая возможные изменения в коде.

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

Посмотрим, как проявляются эти проблемы на примере. Предположим, у нас есть сервис получения поста из блога по его ID — postService.js, посты хранятся в базе данных.

// postService.js
const db = require('./database'); // Модуль для работы с базой данных
 
const getPostById = async (id) => {
  const post = await db.query('SELECT text FROM posts WHERE id = $1', [id]);
  return post.rows[0].text;
};
 
module.exports = { getPostById };

В наших тестах будем мокировать этот модуль базы данных database.js:

// postService.test.js
const { getPostById } = require('./postService');
const db = require('./dataBase');

// Мокируем базу данных через встроенный в jest автоматический мокер
jest.mock('./dataBase');

test('should return post text by id', async () => {
  const mockPost = { rows: [{ text: 'Текст для поста' }] };
 
  // Настраиваем возвращаемое значение для мокируемой функции query
  db.query.mockResolvedValue(mockPost);
 
  const result = await getPostById(1);
 
  expect(result).toBe(''Текст для поста'');
  expect(db.query).toHaveBeenCalledWith('SELECT text FROM posts WHERE id = $1', [1]);
});

Тест может пропустить баг, потому что он полностью изолирован от реальной базы данных. Рассмотрим несколько сценариев, которые могут привести к проблемам в реальном приложении:

  • если структура таблицы posts изменится, тест все равно пройдет, потому что всегда будет возвращаться наш замоканный ответ;

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

  • если SQL-запрос написан с ошибкой, тест это не выявит.

Я попробовала выделить признаки, которые указывают на то, что, возможно, вы злоупотребляете мокированием в своих тестах:

  • Большую часть теста занимает настройка моков, а не проверка функциональности.

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

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

  • Мокируются много классов/функций, или один из ваших моков указывает, как должны вести себя более чем один или два метода.

  • Если вы обнаруживаете, что мысленно проходите по тестируемому коду, чтобы понять суть теста.

Баланс заключается в том, чтобы использовать моки только там, где это действительно необходимо для изоляции тестируемого кода, и стараться минимизировать их влияние на тесты.

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

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

Решением стала замена баз данных и сервисов на их более упрощенные копии. Для всех остальных микросервисов API-тесты писались с нуля уже с использованием этих копий.

Что такое фейки и когда их применять

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

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

Фейки — это упрощенные реализации реальных объектов, которые имитируют их поведение с большей точностью, чем моки. Пример: если вы работаете с API блога, вместо полноценной системы с базой данных можно создать фейковую реализацию, которая хранит посты в памяти. Не нужно взаимодействовать с реальной БД, достаточно хранить все в памяти и работать с объектами через фейк-базы.

Не нужно взаимодействовать с реальной БД, достаточно хранить все в памяти и работать с объектами через фейк-базы
Не нужно взаимодействовать с реальной БД, достаточно хранить все в памяти и работать с объектами через фейк-базы

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

Почему стоит использовать фейки:

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

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

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

  • Проще в использовании, ведь фейки — это копии реального функционала, который уже есть в коде. Их не нужно дополнительно настраивать, как моки.

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

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

Из этого выходит сразу несколько проблем:

  • Реализация фейков требует хорошего понимания функционала и уверенных технических навыков. Привлечение разработчиков/автоматизаторов может быть затруднительным из-за нехватки ресурсов и дополнительной нагрузки на команду. В итоге это может значительно увеличить время написания тестов.

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

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

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

У фейков обязательно должны быть собственные тесты, чтобы гарантировать, что они ведут себя как реальная имплементация. Если реальная реализация выдает исключение при вводе определенных данных, фейк также должен выдавать исключение при вводе тех же данных. Один из подходов написания таких тестов состоит в проверках для фейка и реальной системы: чтобы результаты этих проверок совпадали. Реализация такого подхода — отдельная проблема.

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

Как мы реализовали фейки

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

Реальные вставки кода использовать не могу, поэтому примеры будут немного абстрактными; код представлен для приложений на NestJS и с тестами на Jest; со всеми используемыми функциями в коде можно ознакомиться в официальной документации по NestJS.

В первую очередь мы хотели проверить взаимодействие микросервисов с базами данных и внешними сервисами. В примере рассмотрим замену базы данных на фейковую и дальнейшее ее использование в API-тестах.

У нас есть база данных — пусть это будет БД, работающая со структурами данных типа «ключ — значение». Напишем для наших тестов ее упрощенную копию, которая записывает данные в память. Помним, что методы в фейках обычно реализованы проще, чем реальные методы:

// fakeDatabase
import { Injectable } from '@nestjs/common';
import { Database } from '../database.service'; // настоящая реализация базы данных
 
@Injectable()
export class FakeDatabase extends Database {
   private static cache: any = {}; // здесь будем хранить все данные
   static ready = true; // для того, чтобы задавать состояние базы
 
   // подключение и отключение базы
   private beforeDisconnectCallbacks: ((...args: any[]) => any)[] = [];
 
   addBeforeDisconnectCallback = (callback: (...args: any[]) => any) => {
       this.beforeDisconnectCallbacks.push(callback);
   };
 
   async onModuleDestroy(): Promise<void> {
       await Promise.allSettled(
           this.beforeDisconnectCallbacks.map((cb) => cb())
       );
 
       this.ready = false;
   }
 
   onModuleInit(): any {
       this.ready = true;
   }
 
   // метод для очищения всех данных из памяти
   static flushall() {
       FakeDatabase.cache = {};
   }
 
   // получаем и задаем состояние базы
   get ready() {
       return FakeDatabase.ready;
   }
 
   set ready(value) {
       FakeDatabase.ready = value;
   }
 
   // метод добавления данных в фейковую базу
   static async set(key: string, value: any, options?: { ttl: number }) {
       FakeDatabase.cache[key] = {
           value,
           ttl: options?.ttl ? options.ttl : -1,
           created: Date.now(),
       };
   }
 
   async set(key: string, value: any, options?: { ttl: number }) {
       await FakeDatabase.set(key, value, options);
   }
 
   // метод получения данных из базы
   static async get(key: string): Promise<string | null> {
       const data = FakeDatabase.cache[key] || {};
 
       return typeof data.value !== 'undefined' ? data.value : null;
   }
 
   async get(key: string) {
       return FakeDatabase.get(key);
   }
 
   // срок хранения ключа в базе
   ttl(key: string) {
       return FakeDatabase.cache[key].ttl;
   }
 
   static ttl(key: string) {
       return FakeDatabase.cache[key].ttl || null;
   }
 
   // удаление данных по ключу
   static async del(key: string): Promise<void> {
       delete FakeDatabase.cache[key];
   }
 
   async del(key: string): Promise<void> {
       await FakeDatabase.del(key);
   }
}

Статические методы задаются для централизованного управления данными в фейковой базе. 

Теперь напишем модуль нашего приложения и создадим метод специально для тестов, который будет поднимать приложение, заменяя в нем оригинальную базу на фейковую.

// app.module
import { Module } from '@nestjs/common';
 
// импортируем все модули, нужные для работы всего приложения
import { Database } from '../../common/src/modules/database.module';
…
 
@Module({
   imports: [
       Database,
       …
   ],
   controllers: [],
   providers: [],
})
 
export class AppModule {}

Сам метод:

// createTestModule
import { Module } from '@nestjs/common';
import { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';
import { Type } from '@nestjs/common/interfaces/type.interface';
import {
   FastifyAdapter,
   NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
 
import { FakeDatabase } from '../modules/__fakes__/database.service';
import { Database } from '../modules/database.service';
 
// создаем структуру модуля приложения
type CreateAppModuleType = {
   module:
       | Type<any>
       | DynamicModule
       | Promise<DynamicModule>
       | ForwardReference;
};
 
export const createTestModule = async ({
   module
}: CreateAppModuleType): Promise<NestFastifyApplication> => {
   let moduleFixture = Test.createTestingModule({
       imports: [module],
   })
       // заменяем базу данных его фейком
       .overrideProvider(Database)
       .useClass(FakeDatabase)
   	// заменяем остальные нужные сервисы фейками
       …
 
   const app = (
       await moduleFixture.compile()
   ).createNestApplication<NestFastifyApplication>(new FastifyAdapter()); // создаем приложение
 
   return app;
};

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

// generateCode.service
import { Injectable } from '@nestjs/common';
import { faker } from '@faker-js/faker';
 
import { Database } from '../../../../common/modules/database.service';
 
@Injectable()
export class generateCodeService {
   constructor(
       private readonly db: Database
   ) {}
 
   async getCode(userId: string
): {
       // если база недоступна, то не генерируем код
       if (!this.db.ready) {
           return;
       }
 
       const oldCode = await this.db.get(
           `code:${userId}`
       );
 
  	// если уже есть код, то удаляем его
       if (oldCode) {
           await this.db.del(
               `code:${userId}`
       }
 
       const code = faker.string.numeric({ length: 5 });
 
       // заносим в базу сгенерированный код
       await this.db.set(
           `code:${userId}`,
           code,
           { ttl: 180 }
       );
 
       return code;
   }
}

Напишем для этого метода API-тесты, которые сами поднимают приложение с фейковой базой. Теперь при выполнении кода в generateCode.service вместо взаимодействия с оригинальной базой и ее методами будет использоваться упрощенная копия.

// generateCode.api.test
import { faker } from '@faker-js/faker';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import request from 'supertest';// библиотека для работы с HTTP-запросами  
 
import { createTestModule } from '../../common/test-utils/createTestModule';
 
import { AppModule } from '../app.module';
 
import { FakeDatabase } from '../../common/modules/__fackes__/database.service';
 
describe('Запрос code', () => {
   let app: NestFastifyApplication;
   const userId = faker.internet.userName();
   const key = `code:${userId}`; // ключ, по которому хранится код в фейковой базе
 
   beforeEach(async () => {
       // поднимаем приложение с фейковой базой данных
       app = await createTestModule({
           module: AppModule,
       });
 
       await app.init();
       await app.getHttpAdapter().getInstance().ready();
   });
 
   afterEach(async () => {
       // прекращаем работу поднятого приложения и очищаем все данные после каждого теста
       await app.close();
       FakeDatabase.ready = true;
       FakeDatabase.flushAll();
   });
 
   it('Код не генерируется при недоступности базы данных, async () => {
       expect.assertions(1);
 
       // имитируем состояние базы
       FakeDatabase.ready = false;
 
       return request(app.getHttpServer())
           .post('/generate_code')
           .send({userId})
           .expect(async (res) => {
               expect(res.text).toBe('');              
           });
   });
 
   it('Отправляется лог об отправке запроса, генерируется и возвращается новый код', async () => {
       expect.assertions(2);
 
       return request(app.getHttpServer())
           .post('/generate_code')
           .send({userId})
           .expect(async (res) => {
               const code = await FakeDatabase.get(key);
 
               expect(res.text).toBe(code);
               expect(await FakeDatabase.ttl(key)).toBe(180);
           });
   });
 
   it('Если был старый код, то он удаляется и генерируется новый', async () => {
       expect.assertions(3);
 
       const oldCode = '1234';
 
       await FakeDatabase.set(key, oldCode);
   
       return request(app.getHttpServer())
           .post('/generate_code')
           .send({userId})
           .expect(async (res) => {
               const newCode = await FakeDatabase.get(key);
 
               expect(newCode).not.toBe(oldCode);
               expect(res.text).toBe(newCode);
               expect(await FakeDatabase.ttl(key)).toBe(180);
           });
   });
});

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

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

Процесс написания новых API-тестов и взаимодействие тестирования с разработкой в нашей команде:

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

С фейками трудности обычно возникают поначалу, но как только они реализованы, поддерживать их будет не так сложно. За счет того, что упрощенные копии реализованы на нижних уровнях, изменения в коде происходят редко: методы работы с базой данных особо не меняются. И даже при изменениях часто достаточно поменять что-то в самих фейках, не нужно переписывать или дописывать тесты. У нас фейки дорабатываются в рамках разработки или берутся в работу дежурными, что не отвлекает от основных задач. 

За это время мы написали всего ~ 600 API-тестов для 10 микросервисов с использованием фейков для трех баз данных и двух внешних сервисов. Они учитываются в покрытии кода, что позволяет нам не плодить модульные тесты. Значение coverage составляет в среднем 80% для каждого микросервиса, притом что некоторые из них покрыты только API-тестами. Все эти тесты запускаются разработчиками локально во время работы над задачей, а также настроен их запуск в CI/CD при каждом коммите, помогая разработчикам находить дефекты на ранних стадиях и сразу фиксить их без привлечения тестирования.

Преимущества использования фейков вместо реальных баз данных и сервисов:

  • не нужно настраивать коннекты до баз и сервисов локально + прописывать логику очистки всех тестовых данных;

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

  • время выполнения тестов сократилось примерно в два раза;

  • исключены сбои или ошибки при очистке данных.

Раньше API проверялся вручную в процессе тестирования задачи. Благодаря выстроенному процессу новые методы покрываются автотестами до их передачи в тест. Сейчас один прогон всех API-тестов выполняется за 10 минут и эти прогоны всегда включены в этап разработки задачи. По покрытому функционалу за все это время не было пропущенных на прод багов, что указывает на эффективность написанных тестов.

После перехода на фейки код тестов стал более чистым, тесты не перегружены настройкой моков, как раньше. Перед написанием тестов нужно лишь подготовить тестовые данные, которые будут храниться в памяти и использоваться для фейковых баз и сервисов. Максимум, что придется сделать, — задать состояние самого фейка для определенных кейсов: к примеру, что БД недоступна, как в API-тестах из generateCode.api.test. При использовании же моков пришлось бы настраивать для каждого кейса, что возвращает БД, что возвращает метод и т. д.

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

Заключение

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

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

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

Комментарии (2)


  1. ruomserg
    20.11.2024 20:57

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

    Дальше у меня возникает вопрос - а не переизобретаете ли вы в своих fake-сервисах известное ПО WireMock ? Это же как раз универсальный сервер который может мимикрировать под что угодно, только дайте конфигурацию (программно или через json-файлы). Плюс там есть куча полезных функций типа записи реальных реквестов и ответов в режиме прокси, и прочие плюшки. Вы его не пробовали, или чего-то не хватило ?

    Далее, если вы будете работать с WireMock - то мы пришли к отличной идее, что каждый сервис должен предоставлять библиотеку, которая упрощает конфигурацию fake-ов. Смысл в том, что для того чтобы корректно мокать поведение стороннего сервера, вообще-то надо знать как он работает. И это знание начинает переливаться из одного сервиса в другой. Получается глупость - сначала мы изолируем сервисы друг от друга при помощи open-api спецификаций, а потом снова зацепляем на уровне тестов. А если мы используем для конфигурации мока библиотеку, предоставленную мокируемым сервисом - это до некоторой степени их расцепляет.

    Ну и последнее - это запрет разработчикам придумывать себе объекты для тестов. Вместо этого, создается стандартная библиотека тестовых объектов (в идеале, ее поддерживают BA - чтобы объекты были похожи на реальные с точки зрения наполнения полей). А разработчик в тестах должен загрузить один из этих тестовых объектов (допускаются простые кастомизации типа затереть поле, удалить поле, итд - чтобы не раздувать библиотеку стандартных тестовых объектов до бесконечности) - и с этим уже производить тесты (например мокать данным объектом возврат стороннего сервиса).

    В итоге получается следующая логичная цепочка: бизнес (BA) поддерживает библиотеку стандартных тестовых объектов -> тест загружает и кастомизирует тестовые объекты -> тестовые объекты передаются в библиотеки-хелперы сторонних сервисов -> полученные mock-command выполняются и конфигурируют инстанс WireMock для проведения теста -> тест проводится с максимально реальными объектами и максимально корректно настроенными фейками зависимостей -> профит.


  1. Zukomux
    20.11.2024 20:57

    Судя по тексту вы решаете проблему некачественных моков не путем их правильного и корректного описания, а путем создания дополнительной сущности в виде Fake Database. При этом уж там-то вы не можете ошибиться, правда? Из проблем, которые я тут вижу - это появляется связанность приложения с дополнительной сущностью. Невозможно разрабатывать приложения через подход API First. Как уже правильно было отмечено, моки позволяют моделировать различные ситуации, сложно воспроизводимые в реальности. Появляется дополнительная точка обслуживания/отказа. Я как фронт-разработчик могу, но не обязан разбираться в тонкостях работы с конкретной бд, ее таблицами и запросами. Опять же - наполненность данными. Фейковые данные составляют те же люди, но тут они уже не ошибаются? Ваше утверждение очень спорное. По своему примеру скажу, что у нас на 2 проектах порядка 1500 юнит тестов написанных на моках. Изредка их приходится обслуживать, зачастую это коррекция архитектуры, но это не более 1% от общего числа