Пишем смарт-контракты на FunC - Урок 8 Тестируем HashMap хранилище

Урок 8 Тесты на FunC для смарт-контракта c Hashmap

Введение

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

Требования

Для прохождения данного урока вам достаточно установить Node.js. Желательно устанавливать одну из последних версий, например 18.

А также пройти седьмой урок.

Меняем beforeEach в тестах для упрощения

Откроем файл tests/Hashmap.spec.ts в котором будут находиться наши тесты и поменяем функцию beforeEach, которая выполняется перед каждым тестом.

Добавим в неё установку текущего времени (меняем значение blockchain.now). Помимо этого, после успешного деплоя контракта, сразу попробуем установить три тестовых значения в нашей хешмапе используя написанный ранее метод sendSet.

С этого момента, в начале каждого теста, время уже будет установлено на 500, а также три значения будут уже записаны (или не записаны в случае если смарт-контракт работает некорректно).

Получаем примерно такую функцию:

beforeEach(async () => {
    blockchain = await Blockchain.create();

    blockchain.now = 500;

    deployer = await blockchain.treasury('deployer');

    hashmap = blockchain.openContract(
        Hashmap.createFromConfig(
            {
                manager: deployer.address,
            },
            code
        )
    );

    const deployResult = await hashmap.sendDeploy(
        deployer.getSender(),
        toNano('0.01')
    );

    expect(deployResult.transactions).toHaveTransaction({
        from: deployer.address,
        to: hashmap.address,
        deploy: true,
    });

    await hashmap.sendSet(deployer.getSender(), toNano('0.05'), {
        queryId: 123n,
        key: 1n,
        validUntil: 1000n,
        value: beginCell().storeUint(123, 16).endCell().asSlice(),
    });

    await hashmap.sendSet(deployer.getSender(), toNano('0.05'), {
        queryId: 123n,
        key: 2n,
        validUntil: 2000n,
        value: beginCell().storeUint(234, 16).endCell().asSlice(),
    });

    await hashmap.sendSet(deployer.getSender(), toNano('0.05'), {
        queryId: 123n,
        key: 3n,
        validUntil: 3000n,
        value: beginCell().storeUint(345, 16).endCell().asSlice(),
    });
});

Тестируем запись и получение значений

Помним, что запись значений уже произошла в beforeEach, поэтому здесь нам остаётся лишь проверить, что значения действительно записались корректно.

Для этого используем написанный нами метод getByKey и сравниваем оба значения validUntil и value с ожидаемыми значениями (теми, которые мы записали в контракт).

Замечу, что для сравнения специфичных для TON типов (например Address или Slice), существуют отдельные мэтчеры. В этом случае нам пригодился toEqualSlice, который в тесте сравнивает на равенство два слайса.

Повторим эту процедуру для всех трёх записанных значений и тест готов.

it('should store and retrieve values', async () => {
    let [validUntil, value] = await hashmap.getByKey(1n);
    expect(validUntil).toEqual(1000n);
    expect(value).toEqualSlice(
        beginCell().storeUint(123, 16).endCell().asSlice()
    );

    [validUntil, value] = await hashmap.getByKey(2n);
    expect(validUntil).toEqual(2000n);
    expect(value).toEqualSlice(
        beginCell().storeUint(234, 16).endCell().asSlice()
    );

    [validUntil, value] = await hashmap.getByKey(3n);
    expect(validUntil).toEqual(3000n);
    expect(value).toEqualSlice(
        beginCell().storeUint(345, 16).endCell().asSlice()
    );
});

Тестируем наличие ошибки при не существующем ключе

Гет-методы, как и external сообщения, при неуспешном выполнении выбрасывают ошибку в TypeScript программе. Поэтому здесь нам нужно проверить, что вызов getByKey(123n) завершится с ошибкой. Так как этот метод асинхронный (вызывается с await), этот самый await следует вставить перед expect().

Наличие ошибки при вызове функции можно проверить через .rejects.toThrow().

it('should throw on not found key', async () => {
    await expect(hashmap.getByKey(123n)).rejects.toThrow();
});

Тестируем очистку старых значений

В этом тесте нам пригодится возможность менять значение текущего времени blockchain.now.

Для начала, попробуем вызвать очистку значений, не меняя время. В таком случае, ключ 1 должен успешно найтись.

await hashmap.sendClearOldValues(deployer.getSender(), toNano('0.05'), {
    queryId: 123n,
});

let [validUntil, value] = await hashmap.getByKey(1n);
expect(validUntil).toEqual(1000n);
expect(value).toEqualSlice(beginCell().storeUint(123, 16).endCell().asSlice());

Далее, выставим время на 1001. Так как validUntil у первого ключа равен 1000, после очистки, этот ключ должен пропасть. При этом все оставшиеся ключи должны остаться в контракте и никак не поменяться.

blockchain.now = 1001;

await hashmap.sendClearOldValues(deployer.getSender(), toNano('0.05'), {
    queryId: 123n,
});

await expect(hashmap.getByKey(1n)).rejects.toThrow();

[validUntil, value] = await hashmap.getByKey(2n);
expect(validUntil).toEqual(2000n);
expect(value).toEqualSlice(beginCell().storeUint(234, 16).endCell().asSlice());

[validUntil, value] = await hashmap.getByKey(3n);
expect(validUntil).toEqual(3000n);
expect(value).toEqualSlice(beginCell().storeUint(345, 16).endCell().asSlice());

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

blockchain.now = 3001;

await hashmap.sendClearOldValues(deployer.getSender(), toNano('0.05'), {
    queryId: 123n,
});

await expect(hashmap.getByKey(2n)).rejects.toThrow();
await expect(hashmap.getByKey(3n)).rejects.toThrow();

Весь код этого теста:

it('should clear old values', async () => {
    await hashmap.sendClearOldValues(deployer.getSender(), toNano('0.05'), {
        queryId: 123n,
    });

    let [validUntil, value] = await hashmap.getByKey(1n);
    expect(validUntil).toEqual(1000n);
    expect(value).toEqualSlice(
        beginCell().storeUint(123, 16).endCell().asSlice()
    );

    blockchain.now = 1001;

    await hashmap.sendClearOldValues(deployer.getSender(), toNano('0.05'), {
        queryId: 123n,
    });

    await expect(hashmap.getByKey(1n)).rejects.toThrow();

    [validUntil, value] = await hashmap.getByKey(2n);
    expect(validUntil).toEqual(2000n);
    expect(value).toEqualSlice(
        beginCell().storeUint(234, 16).endCell().asSlice()
    );

    [validUntil, value] = await hashmap.getByKey(3n);
    expect(validUntil).toEqual(3000n);
    expect(value).toEqualSlice(
        beginCell().storeUint(345, 16).endCell().asSlice()
    );

    blockchain.now = 3001;

    await hashmap.sendClearOldValues(deployer.getSender(), toNano('0.05'), {
        queryId: 123n,
    });

    await expect(hashmap.getByKey(2n)).rejects.toThrow();
    await expect(hashmap.getByKey(3n)).rejects.toThrow();
});

Тестируем ошибку при ненайденном опкоде

Для этого воспользуемся методом send у deployer для отправки произвольного сообщения. Отправим, например, opcode = 123 и query_id = 123.

Такая транзакция должна закончиться с exitCode = 12, как мы и прописывали в контракте. Подобные проверки мы уже умеем делать.

it('should throw on wrong opcode', async () => {
    const result = await deployer.send({
        to: hashmap.address,
        value: toNano('0.05'),
        body: beginCell().storeUint(123, 32).storeUint(123, 64).endCell(),
    });
    expect(result.transactions).toHaveTransaction({
        from: deployer.address,
        to: hashmap.address,
        exitCode: 12,
    });
});

Тестируем ошибку при некорректном запросе

Как мы помним, op = 2 в нашем контракте предусматривает ошибку при наличии лишних данных в body сообщения. Это обеспечивается вызовом end_parse().

Для проверки этой ошибки, как и в прошлом тесте, воспользуемся методом send и отправим сообщение с опкодом = 2, но в конец тела добавим так же лишние данные.

Такая транзакция должна закончиться неуспешно, то есть в мэтчере toHaveTransaction добавим флаг success: false.

it('should throw on bad query', async () => {
    const result = await deployer.send({
        to: hashmap.address,
        value: toNano('0.05'),
        body: beginCell()
            .storeUint(2, 32)
            .storeUint(123, 64)
            .storeStringTail('This string should not be here!')
            .endCell(),
    });
    expect(result.transactions).toHaveTransaction({
        from: deployer.address,
        to: hashmap.address,
        success: false,
    });
});

Запускаем тесты

Запустим тесты командой npx blueprint test и мы должны увидеть следующее:

 PASS  tests/Hashmap.spec.ts
  Hashmap
    ✓ should store and retrieve values (173 ms)
    ✓ should throw on not found key (80 ms)
    ✓ should clear old values (95 ms)
    ✓ should throw on wrong opcode (73 ms)
    ✓ should throw on bad query (129 ms)

Если какие то тесты у вас не прошли, просмотрите код и текст этого урока ещё раз. Также сверьте свой код смарт-контракта с кодом из предыдущего урока.

Заключение

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