Jumping into Solidity — The ERC721 Standard (Part 8)

Well guys and gals, here we are, it’s time to test our Metadata and Enumerable extensions. Judging by how many of you skipped the last article on tests (that’s right, I have stats!), I know a lot of you think you can deploy untested smart contracts with no ramifications. Although some of you daredevils may be willing to live that kind of rock-n-roll lifestyle, I’ll have no part in it.

So for all the cautious codeslingers among you who don’t like explaining to users why their NFTs are broken or lost, here are some juicy tests to sink your teeth into.

“person pouring purple liquid on clear glass container” by Louis Reed on Unsplash

The Set-up

Today I’ll be using NodeJS, with the Mocha test framework. We’ll also be making use of Web3 and ganache-cli.

Part 5 includes a full explanation on how to get everything set up, so there’s no need to repeat it here. In fact, we’re going to be re-using all the tests we wrote then anyway, since the Metadata and Enumerable extensions both extend the basic ERC721 standard. Even though we changed the guts of a few functions with our Enumerable contract, the outward functionality is the same, and so the tests are the same.

Because of this we’re actually going to be writing two test files, one for each extension, and both of them are going to start off as duplicates of our original Token.test.js file we wrote in Part 5.

So start by making two duplicates of this file in your /test directory, and name one Token_Metadata.test.js and the other Token_Enumerable.test.js.

Rather than making you jump back and forth between these two test files, we’re going to tackle the whole Metadata test file first, and then come back round and look at Enumerable.

Token_Metadata.test.js

const compiledToken = require('../ethereum/build/TokenERC721.json');

to be:

const compiledToken = require('../ethereum/build/TokenERC721Metadata.json');

Also remember that our Metadata contract also has a few extra parameters in the constructor,

constructor(uint _initialSupply, string _name, string _symbol, string _uriBase)

So we need to make sure we pass them when our contract gets deployed in the beforeEach function. Right above that function, let’s define a couple of constants so we can test against them later,

const _name = "SomeTokenName";
const _symbol = "STN";
const _uriBase = "someTokenName.uri/";

And then inside the beforeEach we want to add them as arguments in the deploy function, by changing:

arguments: [initialTokens]

to be:

arguments: [initialTokens,_name,_symbol,_uriBase]

And those are all the changes we need to make to the set-up. If you run the test file, you should see your Metadata contract passing all the tests we wrote in Part 5. We now just have to add a few extra tests for the Metadata-specific functions.

A few extra Metadata tests

Correct token name is reported

it('Correct token name is reported', async () => {
const name_reported = await token.methods.name().call();

assert(_name == name_reported);
});

Correct token symbol is reported

it('Correct token symbol is reported', async () => {
const symbol_reported = await token.methods.symbol().call();

assert(_symbol == symbol_reported);
});

Correct tokenURI is reported

it('Correct tokenURI is reported',async() => {
var tokenId = '1';
const tokenURI_reported = await token.methods.tokenURI(tokenId).call();
const tokenURI_expected = _uriBase + tokenId;
assert(tokenURI_expected == tokenURI_reported);
});

And that’s it for Metadata! We’ve barely broken a sweat and we’re already almost done. Let’s take care of our Enumerable tests and then we can all go home.

Token_Enumerable.test.js

const compiledToken = require('../ethereum/build/TokenERC721.json');

to be:

const compiledToken = require('../ethereum/build/TokenERC721Enumerable.json');

As far as changes to the set-up go, that’s all there is for Enumerable, so let’s move onto those extra tests.

A few extra Enumerable tests

The latter two are a little more complex given the way our token assigns indices, especially if you have mint and burn functions, so we’ll have to do a little more work in that department.

Reported total supply is accurate for initial supply

it("Reported total supply is accurate for initial supply", async() => {
const totalSupply_reported = await token.methods.totalSupply().call();
assert(totalSupply_reported == initialTokens);
});

And then for you minters and burners, you’ll want something like the following. But I’ll remind you that neither of these methods form part of the standard, so if you’re homebrewing your contract you might need to tweak these a little.

Reported total supply is accurate after minting extra tokens

it("Reported total supply is accurate after minting extra tokens", async() => {
const toIssue = 2;
const owner = accounts[0];
await token.methods.issueTokens(toIssue).send({
from: owner,
gas:'8000000'
});
const totalSupply_expected = initialTokens + toIssue;

const totalSupply_reported = await token.methods.totalSupply().call();
assert(totalSupply_reported == totalSupply_expected);
});

Reported total supply is accurate after burning a token

it("Reported total supply is accurate after burning a token", async() => {
//Remember, this burns an individual token, not a group of tokens.
const tokenToBurn = '2'; //2 is the tokenId
const owner = accounts[0];
await token.methods.burnToken(tokenToBurn).send({
from: owner,
gas:'8000000'
});

const totalSupply_expected = initialTokens - 1;

const totalSupply_reported = await token.methods.totalSupply().call();
assert(totalSupply_reported == totalSupply_expected);
});

Initially reports correct tokenByIndex

it("Initially reports correct tokenByIndex", async() => {
let tokenId_expected, tokenId_reported;
for(var i = 0; i < initialTokens; i++){
tokenId_expected = String(i + 1);
tokenId_reported = await token.methods.tokenByIndex(i).call();
assert(tokenId_expected == tokenId_reported);
}
});

Reports correct tokenByIndex after minting extra tokens

it("Reports correct tokenByIndex after minting extra tokens", async() => {
const toIssue = 2;
const owner = accounts[0];
await token.methods.issueTokens(toIssue).send({
from: owner,
gas:'8000000'
});

let tokenId_expected, tokenId_reported;
for(var i = 0; i < initialTokens + toIssue; i++){
tokenId_expected = String(i + 1);
tokenId_reported = await token.methods.tokenByIndex(i).call();
assert(tokenId_expected == tokenId_reported);
}
});

Reports correct tokenByIndex after burning a token

[1,10,3,4,5,6,7,8,9]

Note, the last element filled the new gap. Rather than bothering to replicate the logic in Javascript, let’s just hard-code this new order and test for that.

it("Reports correct tokenByIndex after burning a token", async() => {
const tokenToBurn = '2'; //2 is the tokenId
const owner = accounts[0];
await token.methods.burnToken(tokenToBurn).send({
from: owner,
gas:'8000000'
});

const tokenIds_expected = ['1','10','3','4','5','6','7','8','9'];

let tokenId_expected, tokenId_reported;
for(var i = 0; i < tokenIds_expected.length; i++){
tokenId_expected = tokenIds_expected[i];
tokenId_reported = await token.methods.tokenByIndex(i).call();
assert(tokenId_expected == tokenId_reported);
}
});

The next three tests are almost identical to the last three

Initially reports correct tokenOfOwnerByIndex

it("Initially reports correct tokenOfOwnerByIndex", async() => {
const owner = accounts[0];
let tokenId_expected, tokenId_reported;
for(var i = 0; i < initialTokens; i++){
tokenId_expected = String(i + 1);
tokenId_reported = await token.methods.tokenOfOwnerByIndex(owner,i).call();
assert(tokenId_expected == tokenId_reported);
}
});

Reports correct tokenOfOwnerByIndex after minting extra tokens

it("Reports correct tokenOfOwnerByIndex after minting extra tokens", async() => {
const toIssue = 2;
const owner = accounts[0];
await token.methods.issueTokens(toIssue).send({
from: owner,
gas:'8000000'
});

let tokenId_expected, tokenId_reported;
for(var i = 0; i < initialTokens + toIssue; i++){
tokenId_expected = String(i + 1);
tokenId_reported = await token.methods.tokenOfOwnerByIndex(owner,i).call();
assert(tokenId_expected == tokenId_reported);
}
});

Reports correct tokenOfOwnerByIndex after burning a token

it("Reports correct tokenOfOwnerByIndex after burning a token", async() => {
const tokenToBurn = '2'; //2 is the tokenId
const owner = accounts[0];
await token.methods.burnToken(tokenToBurn).send({
from: owner,
gas:'8000000'
});

const tokenIds_expected = ['1','10','3','4','5','6','7','8','9'];

let tokenId_expected, tokenId_reported;
for(var i = 0; i < tokenIds_expected.length; i++){
tokenId_expected = tokenIds_expected[i];
tokenId_reported = await token.methods.tokenOfOwnerByIndex(owner,i).call();
assert(tokenId_expected == tokenId_reported);
}
});

Reports correct tokenOfOwnerByIndex after transferring tokens

it("Reports correct tokenOfOwnerByIndex after transferring tokens", async() => {
const person_A = accounts[0];
const person_B = accounts[1];

//Person A sends token 2 to Person B
await token.methods.transferFrom(person_A,person_B,'2').send({
from: person_A,
gas:'8000000'
});
//Person A sends token 4 to Person B
await token.methods.transferFrom(person_A,person_B,'4').send({
from: person_A,
gas:'8000000'
});
//Person B sends token 2 back to Person A
await token.methods.transferFrom(person_B,person_A,'2').send({
from: person_B,
gas:'8000000'
});

const tokenIds_expected_A = ['1','10','3','9','5','6','7','8','2'];
const tokenIds_expected_B = ['4'];

let tokenId_reported, tokenId_expected;
for(let i = 0; i < tokenIds_expected_A.length; i++){
tokenId_expected = tokenIds_expected_A[i];
tokenId_reported = await token.methods.tokenOfOwnerByIndex(person_A,i).call();
assert(tokenId_expected == tokenId_reported);
}
for(let i = 0; i < tokenIds_expected_B.length; i++){
tokenId_expected = tokenIds_expected_B[i];
tokenId_reported = await token.methods.tokenOfOwnerByIndex(person_B,i).call();
assert(tokenId_expected == tokenId_reported);
}
});

Wrapping up

As with last time, if you’re lazy or just want to ensure you haven’t made any mistakes, I’ve uploaded the test files to GitHub, so feel free to steal them.

This is the last article in this series, so thanks for reading, and I hope your non-fungible projects are as exciting as some of the one’s I’ve got brewing. If you have any questions or comments about anything covered here, then I’d be happy to answer them.

Codeslinger. Melbourne based Solidity developer for hire.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store