Юнит-тесты для хеш-функций

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

Базовый принцип тестирования хеш-функций заключается в проверке соответствия известных входных значений заранее вычисленным контрольным суммам. В CryptoJS чаще всего используются алгоритмы MD5, SHA1, SHA256, SHA512.

import CryptoJS from "crypto-js";

const hash = CryptoJS.SHA256("hello").toString();

console.log(hash);

Юнит-тест фиксирует ожидаемое значение:

import CryptoJS from "crypto-js";

test("SHA256 от строки hello", () => {
  const result = CryptoJS.SHA256("hello").toString();
  expect(result).toBe("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
});

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

Использование тестовых векторов

Для SHA256 классическим примером является строка "abc":

test("SHA256 от abc", () => {
  const result = CryptoJS.SHA256("abc").toString();
  expect(result).toBe("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
});

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

Проверка разных алгоритмов хеширования

Юнит-тесты обычно покрывают сразу несколько алгоритмов:

test("MD5 хеширование", () => {
  const result = CryptoJS.MD5("test").toString();
  expect(result).toBe("098f6bcd4621d373cade4e832627b4f6");
});

test("SHA1 хеширование", () => {
  const result = CryptoJS.SHA1("test").toString();
  expect(result).toBe("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3");
});

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

Влияние кодировки на результат хеша

Одна из частых проблем при тестировании — различие в интерпретации строк. CryptoJS работает с UTF-8 по умолчанию, однако ошибки возникают при неявных преобразованиях.

test("UTF-8 строка с кириллицей", () => {
  const result = CryptoJS.SHA256("привет").toString();

  expect(result).toBe("ожидаемое_значение_хеша");
});

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

import CryptoJS from "crypto-js";

const wordArray = CryptoJS.enc.Utf8.parse("привет");
const hash = CryptoJS.SHA256(wordArray).toString();

Тестирование пустых и граничных значений

Особое внимание уделяется краевым случаям:

test("пустая строка SHA256", () => {
  const result = CryptoJS.SHA256("").toString();
  expect(result).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
});

Также тестируются:

  • очень длинные строки
  • строки с символами NULL
  • пробельные символы
  • строки с Unicode-эмодзи
  • бинарные данные (WordArray)

Работа с бинарными данными

CryptoJS оперирует структурой WordArray, что требует отдельного тестирования при работе с байтовыми последовательностями:

test("хеширование бинарного массива", () => {
  const words = CryptoJS.lib.WordArray.create([0x61626300]); // "abc\0"
  const result = CryptoJS.SHA256(words).toString();

  expect(result).toBeDefined();
});

Такие тесты важны при интеграции с файлами, сетевыми потоками и криптографическими протоколами.

Проверка стабильности при сериализации

Хеш-функции должны давать одинаковый результат вне зависимости от способа передачи данных в CryptoJS:

test("строка и WordArray дают одинаковый результат", () => {
  const hash1 = CryptoJS.SHA256("data").toString();
  const hash2 = CryptoJS.SHA256(CryptoJS.enc.Utf8.parse("data")).toString();

  expect(hash1).toBe(hash2);
});

Это защищает от ошибок при рефакторинге слоёв кодирования.

Использование Jest и Mocha для тестирования

На практике чаще всего применяются Jest или Mocha. В Jest тесты хеш-функций выглядят компактно:

describe("CryptoJS hash tests", () => {
  it("SHA256 consistency", () => {
    const input = "consistent input";
    const hash1 = CryptoJS.SHA256(input).toString();
    const hash2 = CryptoJS.SHA256(input).toString();

    expect(hash1).toBe(hash2);
  });
});

Mocha в связке с Chai:

const { expect } = require("chai");
const CryptoJS = require("crypto-js");

describe("SHA256", () => {
  it("should produce deterministic hash", () => {
    const result = CryptoJS.SHA256("test").toString();
    expect(result).to.equal("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
  });
});

Проверка устойчивости к изменениям входных данных

Даже минимальное изменение строки должно приводить к полностью другому хешу:

test("изменение одного символа меняет хеш", () => {
  const hash1 = CryptoJS.SHA256("data1").toString();
  const hash2 = CryptoJS.SHA256("data2").toString();

  expect(hash1).not.toBe(hash2);
});

Это проверяет фундаментальное свойство лавинного эффекта.

Табличные наборы тестов

Для масштабирования тестирования используется подход data-driven:

const vectors = [
  { input: "a", expected: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" },
  { input: "abc", expected: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" }
];

vectors.forEach(({ input, expected }) => {
  test(`SHA256(${input})`, () => {
    const result = CryptoJS.SHA256(input).toString();
    expect(result).toBe(expected);
  });
});

Этот подход упрощает расширение тестового покрытия без дублирования кода.

Проверка совместимости с внешними реализациями

Важный аспект — соответствие результатов CryptoJS и других реализаций (OpenSSL, Node.js crypto):

import crypto from "crypto";

test("сравнение с Node crypto", () => {
  const input = "comparison";

  const nodeHash = crypto.createHash("sha256").update(input).digest("hex");
  const cryptoJSHash = CryptoJS.SHA256(input).toString();

  expect(cryptoJSHash).toBe(nodeHash);
});

Такие тесты выявляют расхождения в кодировке или обработке входных данных.

Проверка регрессий при обновлении библиотеки

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

Фаззинг и генеративное тестирование

Дополнительный уровень проверки — случайные входные данные:

function randomString(length) {
  return Array.from({ length }, () =>
    String.fromCharCode(Math.floor(Math.random() * 256))
  ).join("");
}

test("фаззинг SHA256", () => {
  for (let i = 0; i < 1000; i++) {
    const input = randomString(50);
    const hash = CryptoJS.SHA256(input).toString();

    expect(hash).toHaveLength(64);
  }
});

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