Identify Anti-patterns within Smart Contract Testing

Noah Hein
InstructorNoah Hein

Share this video with your friends

Send Tweet
Published 7 months ago
Updated 5 months ago

Now that we tested a smart contract in solidity with fuzzed inputs, we'll move on to testing our smart contracts in an idiomatic fashion. We will delete the negative test case for the decrement function, and will replace it with a function that expects a specific error instead of any generic failure. The vm object is used to do this, and we'll pick up on the vocabulary word "cheatcode" which is a reference to the various utilities that forge provides you through the vm object.

Here are more examples of how cheatcodes are used.

And here is the reference sheet for an overview of all the available cheatcodes.

Noah Hein: [0:01] This is good, but it's not great to test that your contract isn't working. We would like to get a little bit more specific because it's good to know that it's not working, but you want to know why it isn't working. [0:16] I would say, generally, that this test fail is a bit of an anti-pattern. You're not always able to exactly pin down why it's failing, so this is a stopgap solution.

[0:26] What you really want to be doing is test. Cannot is typically the word that you would use. Cannot doesn't have any actual value. It's still just a regular test that's going to expect something to be true. We'll say testCannotDecrementLargerNumbers(). Here we'll again uint num, and it will be a public contract.

[0:54] Here it's going to be these exact three lines again. What we want to do is we have this assumption here, but the VM has a lot of what Forge calls cheat codes. What we can do is we can expect it to revert. What this will do is it will say, "Hey, we expect the very next thing to cause something to revert." It will also say, "Hey, what do we expect that revert to be?"

[1:28] Here, we'll leave this blank to see what runs. If we open this up, we can see that if we run for each test, we can see that failed and, which one failed? It looks like the last one did. The test cannot decrement larger numbers. We'll go ahead and copy this and we'll dig into this a little bit.

[1:50] We'll do Forge test, and we'll pass a flag here. You can do -vvvv and this will add a debugger or tracer to it. Then since we don't want to make things too noisy, we'll do --match-test and then we'll pass in our test right here.

[2:08] Here, you can see that it only ran the one test, and we also attached a debugger here. You can see that we ran our setup successfully. Cool. Then on this test, we got the number. It was 10. We made an assumption, and then we did another getNum.

[2:24] Then, whenever we did the expectRevert, you can see the actual error here. It was an arithmetic under/overflow. Here you can see that the reason went from our custom error to this. This arithmetic under/overflow is really what's happening under the hood a little bit.

[2:48] What we want to do is we have access to that class of error. We'll say stdError. We'll call this an arithmeticError. Now, we can see that whenever this passes or whenever we run our test, again, you could see that it passes, but it's giving us some warnings about our unused variables because we're not using the contractNum here.

[3:18] We don't need to use that. We can assume that the number is going to be larger. Then, we can know that this is going to revert. We don't even want to do all of that. We want to test the entire suite right here. Whenever we run forge test, you could see that all of our things are passing.

[3:41] Here, the only other one, you can see that it's also saying that we are not using this. Like I said, this is a bit of an anti-pattern so we will remove that test entirely. Now, whenever we have an entire passing test suite with no warnings and no errors, we know that our contract is both incrementing and decrementing and having a specific error that gets thrown whenever the decrement happens.