Skip to Content
Scalus Club is now open! Join us to get an early access to new features πŸŽ‰

Cardano Smart Contract Testing

Scalus provides comprehensive testing tools for Cardano smart contracts β€” from unit tests through property-based testing to automated attack simulation.

Testing Tools Overview

ToolPurposeBest For
TDD & ATDD WorkflowTest-first development methodologyDefining behavior before implementation
Unit TestingScalusTest trait, assertScriptFail, budget testingValidator logic, script context, execution costs
Property-Based TestingScalaCheck forAll with random Cardano typesEdge cases, invariant verification
Boundary TestingTransaction variations & attack simulationVulnerabilities, multi-step state exploration
DebuggingIDE debugging, logging, error tracesFinding and fixing bugs
EmulatorIn-memory Cardano nodeFast iteration, CI/CD
JS/TS EmulatorEmulator for JavaScript/TypeScriptBrowser DApps, Node.js tooling
Local DevnetDocker-based real Cardano nodeIntegration tests, pre-deployment

Unit Testing

Mix ScalusTest into your test suite for validator testing with script context builders, result checking, and budget assertions:

class MyValidatorTest extends AnyFunSuite, ScalusTest { test("validator accepts valid input") { val result = contract(scriptCtx.toData).program.evaluateDebug assert(result.isSuccess) assert(result.budget == ExUnits(memory = 42970, steps = 16_307848)) } test("reject invalid input") { assertScriptFail("Expected error message") { txCreator.buildTx(invalidInput) } } }

See Unit Testing for the full guide including Party helpers, Emulator setup, and budget tracking.

Property-Based Testing

Use ScalaCheck forAll to verify invariants across hundreds of random inputs:

class MyValidatorTest extends AnyFunSuite, ScalusTest, ScalaCheckPropertyChecks { test("bids at or below current highest are rejected") { forAll(Gen.choose(0L, state.currentBid)) { lowBid => val result = Try(evaluateValidator(lowBid)) assert(result.isFailure) } } }

Scalus provides Arbitrary instances for all Cardano types β€” TxInfo, Value, Address, PubKeyHash, and more. See Property-Based Testing for custom generators and validator property examples.

Boundary & Attack Testing

Automatically explore transaction variations around boundary values and run pre-built attack patterns:

object AuctionStep extends ContractStepVariations[AuctionState] { def extractState(reader: BlockchainReader)(using ExecutionContext) = ... def makeBaseTx(reader: BlockchainReader, state: AuctionState)(using ExecutionContext) = ... def variations = TxVariations.standard.default[AuctionState]( extractUtxo = _.auctionUtxo, extractDatum = s => updatedDatum(s.currentBid + 1, Alice), redeemer = _ => BidRedeemer.toData, script = auctionScript ) }

Includes steal, partial theft, corrupted datum, double satisfaction, and more. Run with ScalaCheck Commands (random multi-step sequences) or Scenario (exhaustive exploration). See Boundary Testing for the full guide.

Debugging

Debug validators as regular Scala code β€” use IDE breakpoints, step through execution, and inspect variables:

@Compile object MyValidator extends Validator: inline override def spend(...): Unit = { log("Starting validation") val owner = datum.getOrFail("No datum").to[PubKeyHash] // Set breakpoint here, inspect variables require(tx.signatories.contains(owner), "Not signed") }

See Debugging for IDE setup and logging.

Local Execution Environments

Emulator β€” Fast In-Memory Testing

The Emulator validates transactions and executes Plutus scripts instantly β€” no Docker required:

val emulator = Emulator.withAddresses(Seq(Alice.address, Bob.address)) val tx = TxBuilder(testEnv) .payTo(Bob.address, Value.ada(10)) .complete(emulator, Alice.address) .await() .sign(Alice.signer) .transaction emulator.submit(tx).await() // Instant validation + script execution

Local Devnet β€” Real Cardano Node

Local Devnet runs a real Cardano node (Yaci DevKit) in Docker:

class MyTest extends AnyFunSuite with YaciDevKit { test("submit transaction to local devnet") { val ctx = createTestContext() val tx = TxBuilder(ctx.cardanoInfo) .payTo(recipient, Value.ada(10)) .complete(ctx.provider, ctx.address) .await(30.seconds) .sign(ctx.signer) .transaction ctx.submitTx(tx) // Real Cardano node validation } }

Test pyramid: Tests β†’ Emulator β†’ Local Devnet β†’ Testnet β†’ Mainnet

1. Write acceptance tests (ATDD) β€” full transaction scenarios 2. Write unit tests (TDD) β€” validator behavior per action 3. Implement validator β€” iterate until tests pass 4. Add boundary & attack testing β€” catch what you didn't think of 5. Deploy β€” Emulator β†’ Devnet β†’ Testnet β†’ Mainnet

See TDD & ATDD Workflow for the full methodology.

See Also

Last updated on