При разработке приложений с использованием библиотеки Globalize часто возникает необходимость тестировать код, связанный с локализацией, форматированием дат, чисел, валют и сообщений. Прямая зависимость от реальных CLDR-данных делает тесты тяжёлыми, медленными и нестабильными. Для изоляции поведения используются моки (mocks) и стабы (stubs).
Моки и стабы позволяют:
Stub заменяет функцию или метод заранее определённым поведением.
Пример:
const stub = sinon.stub(Globalize, "locale");
stub.returns("ru");
В данном случае метод всегда возвращает "ru" независимо
от входных параметров.
Основная задача stub:
Mock не только подменяет поведение, но и проверяет взаимодействие:
Пример:
const mock = sinon.mock(Globalize);
mock.expects("locale")
.once()
.withArgs("ru");
Mock используется для тестирования контрактов взаимодействия.
Библиотека использует большие наборы данных Unicode CLDR:
Globalize.load(require("cldr-data/main/ru/numbers.json"));
Такие данные:
Globalize хранит текущую локаль глобально:
Globalize.locale("ru");
Это приводит к проблемам:
Реальные CLDR-данные не всегда позволяют легко воспроизвести ошибку:
Моки позволяют искусственно создавать такие ситуации.
Наиболее популярная библиотека.
npm install sinon --save-dev
Встроенные возможности мокирования.
npm install jest --save-dev
Альтернативная библиотека для подмен.
function getCurrentLocale() {
return Globalize.locale().locale;
}
const sinon = require("sinon");
describe("Locale", () => {
let localeStub;
beforeEach(() => {
localeStub = sinon.stub(Globalize, "locale");
});
afterEach(() => {
localeStub.restore();
});
it("returns ru locale", () => {
localeStub.returns({
locale: "ru"
});
const result = getCurrentLocale();
expect(result).toBe("ru");
});
});
function formatPrice(price) {
const formatter = Globalize.numberFormatter({
minimumFractionDigits: 2
});
return formatter(price);
}
describe("formatPrice", () => {
let formatterStub;
beforeEach(() => {
formatterStub = sinon.stub(Globalize, "numberFormatter");
});
afterEach(() => {
formatterStub.restore();
});
it("formats number", () => {
formatterStub.returns(value => `formatted:${value}`);
const result = formatPrice(100);
expect(result).toBe("formatted:100");
});
});
sinon.stub(Globalize, "locale").throws(
new Error("E_MISSING_CLDR")
);
function safeLocale() {
try {
return Globalize.locale();
} catch (e) {
return "fallback";
}
}
Тест:
it("returns fallback on error", () => {
sinon.stub(Globalize, "locale")
.throws(new Error("Locale error"));
const result = safeLocale();
expect(result).toBe("fallback");
});
Globalize поддерживает ICU MessageFormat.
Пример:
const formatter = Globalize.messageFormatter("hello");
Тесты не должны зависеть от реальных JSON-файлов переводов.
sinon.stub(Globalize, "messageFormatter")
.returns(name => `mock:${name}`);
function translate(name) {
const formatter =
Globalize.messageFormatter("hello");
return formatter({
name
});
}
Тест:
describe("translate", () => {
beforeEach(() => {
sinon.stub(Globalize, "messageFormatter")
.returns(data => `Hello ${data.name}`);
});
afterEach(() => {
Globalize.messageFormatter.restore();
});
it("translates message", () => {
expect(translate("Alex"))
.toBe("Hello Alex");
});
});
Метод:
Globalize.load(...)
обычно вызывается при инициализации приложения.
В unit-тестах загрузка реальных файлов:
sinon.stub(Globalize, "load");
it("loads cldr data", () => {
const loadStub =
sinon.stub(Globalize, "load");
initializeI18n();
expect(loadStub.called).toBe(true);
loadStub.restore();
});
Globalize.locale("ru");
Меняет состояние глобально.
afterEach(() => {
Globalize.locale("en");
});
const sandbox = sinon.createSandbox();
describe("i18n", () => {
let sandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
it("uses mocked locale", () => {
sandbox.stub(Globalize, "locale")
.returns({
locale: "de"
});
});
});
Sandbox автоматически восстанавливает оригинальные методы.
Globalize.locale = jest.fn();
Globalize.locale.mockReturnValue({
locale: "fr"
});
expect(Globalize.locale)
.toHaveBeenCalled();
describe("locale service", () => {
beforeEach(() => {
Globalize.locale = jest.fn();
});
it("returns current locale", () => {
Globalize.locale.mockReturnValue({
locale: "it"
});
const result =
getCurrentLocale();
expect(result).toBe("it");
});
});
function formatDate(date) {
const formatter =
Globalize.dateFormatter({
datetime: "medium"
});
return formatter(date);
}
sinon.stub(Globalize, "dateFormatter")
.returns(() => "01.01.2025");
it("formats date", () => {
const result =
formatDate(new Date());
expect(result)
.toBe("01.01.2025");
});
function formatCurrency(value) {
const formatter =
Globalize.currencyFormatter("USD");
return formatter(value);
}
describe("currency", () => {
beforeEach(() => {
sinon.stub(Globalize, "currencyFormatter")
.returns(value => `$${value}`);
});
afterEach(() => {
Globalize.currencyFormatter.restore();
});
it("formats currency", () => {
expect(formatCurrency(50))
.toBe("$50");
});
});
async function init() {
const data =
await fetch("/i18n/ru.json");
const json =
await data.json();
Globalize.loadMessages(json);
}
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({
hello: "Привет"
})
})
);
it("loads translations", async () => {
Globalize.loadMessages = jest.fn();
await init();
expect(Globalize.loadMessages)
.toHaveBeenCalled();
});
Иногда требуется подменить только часть API.
const original =
Globalize.numberFormatter;
sinon.stub(Globalize, "numberFormatter")
.callsFake((options) => {
if (options.currency) {
return () => "$100";
}
return original(options);
});
const stub =
sinon.stub(Globalize, "locale");
Globalize.locale("ru");
sinon.assert.calledWith(
stub,
"ru"
);
sinon.assert.calledOnce(stub);
Globalize.locale("ru")
.numberFormatter();
sinon.stub(Globalize, "locale")
.returns({
numberFormatter: () => value =>
`formatted:${value}`
});
Вместо настоящих CLDR-файлов используются минимальные данные.
const fakeCldr = {
main: {
ru: {
identity: {
language: "ru"
}
}
}
};
function mockFormatter(result) {
return sinon.stub()
.returns(() => result);
}
sinon.stub(Globalize, "dateFormatter")
.callsFake(
mockFormatter("fake-date")
);
function getLocale(locale) {
try {
Globalize.locale(locale);
return locale;
} catch {
return "en";
}
}
it("falls back to en", () => {
sinon.stub(Globalize, "locale")
.throws();
expect(getLocale("unknown"))
.toBe("en");
});
Плохо:
Globalize.load(
require("cldr-data")
);
Причины:
Опасный код:
sinon.stub(Globalize, "locale");
Без:
restore()
stub остаётся активным для следующих тестов.
Плохо:
global.Globalize = {};
Это ломает внутреннее API библиотеки.
Лучше подменять только необходимые методы.
Моки полезны для unit-тестов, однако интеграционные тесты должны использовать реальные данные.
Globalize.numberFormatter()(100);
во всех местах приложения.
export function formatNumber(value) {
return Globalize
.numberFormatter()(value);
}
Теперь тестируется только wrapper.
function createPriceService(formatter) {
return {
format(price) {
return formatter(price);
}
};
}
const formatter =
sinon.stub().returns("$100");
const service =
createPriceService(formatter);
expect(service.format(100))
.toBe("$100");
if (Globalize.locale().locale === "ru")
function isRussian(locale) {
return locale === "ru";
}
expect(formatPrice(100))
.toMatchSnapshot();
Форматирование зависит от:
Из-за этого snapshot-тесты могут становиться нестабильными.
tests/
├── mocks/
│ ├── globalize.js
│ ├── cldr.js
│ └── messages.js
module.exports = {
locale: jest.fn(),
numberFormatter: jest.fn(),
dateFormatter: jest.fn(),
currencyFormatter: jest.fn()
};
sinon.stub(Globalize, "pluralGenerator")
.returns(value => {
return value > 1
? "other"
: "one";
});
function getPlural(count) {
const plural =
Globalize.pluralGenerator();
return plural(count);
}
Тест:
expect(getPlural(1))
.toBe("one");
Использование моков существенно снижает:
Особенно заметно это в больших monorepo и CI/CD-системах.
Мокируется только необходимое API.
Каждый тест:
Sandbox упрощает:
Не следует смешивать:
Работа с Globalize через собственный abstraction layer: