- Standard assertions (
assertEq
,assertTrue
, etc.) - Cheatcodes (
vm.prank
,vm.deal
,vm.warp
, etc.) - Console logging
- Test setup with
setUp()
function
Credible Layer Testing Interface
The Credible Layer extends Forge’s testing capabilities with additional utilities specifically for testing assertions. We want to keep the interface minimal and easy to use:Using the Testing Interface
Here’s how to use these utilities in your tests:- Use
cl.assertion()
to register assertions that will be run on the next transaction - Only one assertion function can be registered at a time, which allows you to test assertion functions in isolation
- Use
vm.prank()
to simulate a transaction from a specific address - Use
vm.expectRevert()
to verify that the assertion reverts when expected vm.expectRevert()
allows you to verify the exact error message returned by the assertion- The next transaction after
cl.assertion()
will be validated against the assertion - State changes caused by a non-reverting transaction will be persisted
Testing Assertion Contracts with Constructor Arguments
Most of the time it’s best practice to useph.getAssertionAdopter()
to get the assertion adopter instead of explicitly setting it in the constructor of the assertion contract.
There might however be cases where you want to define additional values that your assertion contract should have access to. For example, this could be the address of a token.
In this case you need to append the ABI-encoded constructor arguments to the contract bytecode.
The abi.encodePacked()
function concatenates the bytecode with the encoded arguments, creating the complete deployment data needed to deploy the contract with the correct constructor parameters.
type(OwnableAssertion).creationCode
- the contract bytecodeabi.encode(address(token))
- the encoded constructor arguments
Testing Patterns
This section shows useful patterns that can be used when testing assertions.Batch Operation Testing Pattern
This pattern is used to test assertions that need to verify behavior across multiple operations in a single transaction. It involves creating a helper contract that performs multiple operations in a single function, allowing us to test the assertion’s ability to track and validate the cumulative effect of these operations.Key Components:
- A helper contract with a function that performs multiple operations
- Testing by calling the helper contract’s function with empty calldata
- Verification of the final state after all operations
Example Implementation:
If you use a fallback function, you need to call it with
address(batchUpdater).call("")
.
This is not the recommended way to do batch operations, since it’s a low level call and it doesn’t properly bubble up the revert reason. We recommend using the pattern above instead.If you do use a fallback function, you can use the following pattern to check the result of the batch operation:Benefits:
- Tests multiple operations in a single transaction
- Verifies that assertions can track cumulative effects
- More realistic testing of real-world scenarios
Protocol Mocking
When testing assertions, it can be difficult to trigger the conditions that would cause an assertion to fail in a real protocol. This is because protocols are designed to be secure and prevent invalid states from occurring. Protocol mocking provides a solution by creating simplified versions of protocols that can be intentionally put into invalid states. For example, you might create a mock protocol that allows direct manipulation of balances or total supply to test if your assertion correctly catches these invalid states.Key Components:
- Create a mock protocol with simplified logic
- Test by intentionally putting the protocol into an invalid state
- Verify that the assertion correctly catches the invalid state