Как издеваться над модулем, который неявно зависит от теста в шутку?

#javascript #typescript #testing #jestjs #integration-testing

#javascript #typescript #тестирование #jestjs #интеграция-тестирование

Вопрос:

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

У Jest есть jest.doMock функция, но это полезно только тогда, когда я хотел импортировать функцию в тест, но в моем случае это функция DB, которую я хотел смоделировать для этого конкретного теста, когда вызывается внутри промежуточного программного обеспечения express.

Есть еще один вариант издевательства над всем ../db модулем, но это сильно усложнит тесты в моем реальном проекте. Для меня было бы очень легко, если бы я мог издеваться над вызовом DB для определенного теста, а для остальных всех тестов он должен выполнять реальные вызовы DB.

Есть ли способ сделать это в шутку?

 // a.ts
import express from "express"
import db from "../db";

const app = express()

app.get("/api/deduct-balance/:txn_id", (req, res) => {
  const txn = await db.findById(txn_id)
  
  // return error message if txn expired
  if (txn.exipre_at <= new Date()) {
    return res.status(401).json({ error: "txn expired" });
  }

  // otherwise update the txn state
  txn.state = "DEDUCTED";
  await txn.save()

  return res.status(200).json();
});
  
 // a.test.ts
import db from "../db";

describe("mixed tests", () => {
  test("should make REAL db calls", async () => {
    await axios.get("/api/deduct-balance/123")
    const txn = await db.findById("123");
    expect(txn.state).toBe("DEDUCTED");
  });

  test("should use MOCKED value", async () => {
    // need a way to mock the DB call so that I can return an expired transaction
    // when I hit the API

    const { data } = await axios.get("/api/deduct-balance/123")
    
    expect(data).toBe({
      error: {
        message: "txn expired"
      }
    });
  });
})
  

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

1. Такого рода тесты лучше не выполнять как интеграционные тесты. Если вы хотите протестировать поведение обработчика запроса, вы должны издеваться над всеми зависимостями, чтобы создать предсказуемый и повторяемый тест. Модуль db должен быть полным макетом и findById должен возвращать фиктивную транзакцию и так далее…

2. @Bart тогда какой тип тестов я должен выполнять в качестве интеграционных тестов? Интересно узнать, что вы думаете об этом.

3. Я оставил ответ об общей сути. К сожалению, мне нужно идти, но я могу расширить свой ответ на более позднем этапе, чтобы сделать его более понятным.

Ответ №1:

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

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

 // deduct-balance-handlers.ts
export const deductBalanceByTransaction = async (req, res) => {
   const txn = await db.findById(txn_id)

   // return error message if txn expired
   if (txn.exipre_at <= new Date()) {
        return res.status(401).json({ error: "txn expired" });
   }

   // otherwise update the txn state
   txn.state = "DEDUCTED";
   await txn.save()

   return res.status(200).json();
}
  

Это также сделает конфигурацию приложения более чистой.

 // a.ts
import express from "express"
import db from "../db";
import { deductBalanceByTransaction } from './deduct-balance-handlers';
const app = express()

app.get("/api/deduct-balance/:txn_id", deductBalanceByTransaction);
  

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

 // a.test.ts
import db from "../db";
import { deductBalanceByTransaction } from './deduct-balance-handlers';

jest.mock('../db');

describe("deduct-balance", () => {
  test("Expired transaction should respond with 401 status", async () => {
    const response = mockResponse();
    deductBalanceByTransaction(request, response);
    expect(response.status).toBe(401);
  });
})
  

Для простоты я оставил часть создания макетного ответа и издевательства над модулем из кода. Больше можно узнать о издевательствах здесь: https://jestjs.io/docs/en/manual-mocks

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

1. Я действительно ценю ответ @Bart, но я намеренно сохранил фрагменты кода примера небольшими, чтобы упростить объяснение. В реальном проекте это намного сложнее, и именно поэтому мне нужен интеграционный тест. Подход, который вы упомянули выше, я использую во многих своих тестах, но моя главная проблема здесь заключается в том, могу ли я издеваться над модулями, которые неявно зависят только в одном тесте. Если вы можете сказать мне, возможно ли это в шутку или нет, тогда это было бы очень полезно. 🙂

2. Одна вещь, которую я бы рассмотрел в первую очередь, это подумать об архитектуре кода. Мешает ли это тестированию? Если это так, у вас есть указание на то, что в дизайне кода что-то не так. В случае вашего примера у обработчика много обязанностей, таких как общение с базой данных и принятие решения о крайних случаях. Обычно обработчики запросов выполняют только одну вещь, обрабатывая ввод (запрос) и вывод (ответ) объектов уровня сервиса. Он не должен делать ничего другого, поскольку это транспортный уровень HTTP. Если дизайн прост, у вас будет меньше зависимостей и вы будете меньше иметь дело с макетами.

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

4. Хорошо, предполагая, что мой дизайн кода не очень хорош, и я хотел его изменить. Есть ли у вас определенные библиотеки с открытым исходным кодом в качестве примера, предпочтительно серверных проектов? Или чей-то другой, который я могу искать для определенного подхода.

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