![]() |
VOOZH | about |
dotnet add package R3E.SmartContract.Testing --version 3.0.0
NuGet\Install-Package R3E.SmartContract.Testing -Version 3.0.0
<PackageReference Include="R3E.SmartContract.Testing" Version="3.0.0" />
<PackageVersion Include="R3E.SmartContract.Testing" Version="3.0.0" />Directory.Packages.props
<PackageReference Include="R3E.SmartContract.Testing" />Project file
paket add R3E.SmartContract.Testing --version 3.0.0
#r "nuget: R3E.SmartContract.Testing, 3.0.0"
#:package R3E.SmartContract.Testing@3.0.0
#addin nuget:?package=R3E.SmartContract.Testing&version=3.0.0Install as a Cake Addin
#tool nuget:?package=R3E.SmartContract.Testing&version=3.0.0Install as a Cake Tool
The R3E.SmartContract.Testing project is designed to facilitate the development of unit tests for smart contract developers in neo, it does not require the project to be done in C#, as it is possible to export artifacts from an Abi.
The process of generating the artifacts, or the source code necessary to interact with the contract, is extremely simple. There are two main ways to do it:
Using the ContractManifest of a contract, the necessary source code to interact with the contract can be generated by calling the GetArtifactsSource method available in the R3E.SmartContract.Testing.Extensions.ArtifactExtensions class, we will only have to specify the name of our resulting class, which will usually be the same as the one existing in the Name field of the manifest.
Through the Neo C# compiler, automatically when compiling a contract in C#, the necessary source code to interact with the contract is generated. This is available in the same path as the generated .nef file, and its extension are .artifacts.cs and .artifacts.dll. Using the --generate-artifacts argument in R3E.Compiler.CSharp followed by the type of artifacts, with the options being: none, source, library, and all.
[TestClass]
public class MyUnitTestClass
{
[TestMethod]
public void GenerateNativeArtifacts()
{
foreach (var n in Native.NativeContract.Contracts)
{
var manifest = n.Manifest;
var source = manifest.GetArtifactsSource();
File.WriteAllText($"{manifest.Name}.cs", source);
}
}
}
The TestEngine class is the main class of the library, providing a simple and intuitive interface for testing smart contracts.
The publicly exposed read-only properties are as follows:
TestEngine and defines the configuration values of the test environment. It defaults to the current blockchain protocol.Signer defined in the Transaction object.ScriptContainer for the neo virtual machine, by default it updates the script of the same as calls are composed and executed, and the Signers will be used as validators for the CheckWitness, regardless of whether the signature is correct or not, so if you want to test with different wallets or scopes, you do not need to sign the transaction correctly, just set the desired signers.Snapshots as well as reverting them. Allows access to the storage of contracts, as well as manually altering their state. It's worth noting that a storage class is provided, which allows for reading the storage from an RPC endpoint. The class in question is named RpcStore and is available in the namespace R3E.SmartContract.Testing.Storage.Rpc.And for read and write, we have:
NetworkFee of the Transaction object.string type.CallFlags for the mocked function, All by default.It has four methods:
ContractManagement.deploy. It allows setting custom mocks, which will be detailed later. And returns the instance of the contract that has been deployed.NefFile or Manifest, only requiring the contract's hash. It does not consider whether the contract exists on the chain unless checkExistence is set to true.Signer of the Transaction.Signer signed by default by CalledByEntry.NefFile and Manifest.// Create the engine initializing the native contracts
var engine = new TestEngine(true);
// Instantiate neo contract from native hash, (not necessary if we use engine.Native.NEO)
var neo = engine.FromHash<NeoToken>(engine.Native.NEO.Hash, false);
// Ensure that the main address contains the totalSupply
Assert.AreEqual(100_000_000, neo.TotalSupply);
Assert.AreEqual(neo.TotalSupply, neo.BalanceOf(engine.ValidatorsAddress));
This class provides precompiled artifacts for neo's native contracts, thereby simplifying and facilitating calls to native contracts.
It has only one method:
Storage that already contains chain data and these contracts have been initialized, calling this method should be avoided. The commit argument determines whether to commit to the active Snapshot of the TestStorage (default is false).// Create the engine initializing the native contracts
var engine = new TestEngine(true);
// Ensure that the main address contains the totalSupply
Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply);
Assert.AreEqual(engine.Native.NEO.TotalSupply, engine.Native.NEO.BalanceOf(engine.ValidatorsAddress));
Avoids dealing with prefixes foreign to the internal behavior of the storage, focusing the developer solely on accessing the storage of the contract, just as it is managed by the smart contract itself, allowing reading, injecting, and deleting entries of the contract in question.
Mainly exposes the methods Import, Export, Contains, Get, Put, and Remove, all of them responsible for reading and manipulating the contract's information.
// Defines the prefix used to store the registration price in neo
byte[] registerPricePrefix = new byte[] { 13 };
// Create engine and initialize native contracts
TestEngine engine = new(true);
// Check previous data
Assert.AreEqual(100000000000, engine.Native.NEO.RegisterPrice);
// Alter data
engine.Native.NEO.Storage.Put(registerPricePrefix, BigInteger.MinusOne);
// Check altered data
Assert.AreEqual(BigInteger.MinusOne, engine.Native.NEO.RegisterPrice);
Storage checkpoints can be created, allowing for a return to specific moments in the execution. This can be achieved with checkpoints.
To create a checkpoint, simply call Checkpoint() from a EngineStorage class or from our TestEngine.
It has the following methods:
EngineStorage or from our TestEngine class. It is used to restore the storage to a specified checkpoint.byte[].Stream.// Create a new test engine with native contracts already initialized
var engine = new TestEngine(true);
// Check that all it works
Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply);
// Create checkpoint
var checkpoint = engine.Storage.Checkpoint();
// Create new storage, and restore the checkpoint on it
var storage = new EngineStorage(new MemoryStore());
checkpoint.Restore(storage.Snapshot);
// Create new test engine without initialize
// and set the storage to the restored one
engine = new TestEngine(false) { Storage = storage };
// Ensure that all works
Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply);
Custom mocks allow redirecting certain calls to smart contracts so that instead of calling the underlying contract, the logic is redirected to a method in .NET, allowing the developer to test in complex environments without significant issues.
Imagine that our project checks that our account has a balance of 123 NEO. It would be enough to redirect the calls to the NEO balanceOf method in the following way, so that it always returns 123.
It's important to note that all syscalls going to this contract will also be redirected, not only the calls to the method in .NET.
// Initialize TestEngine and native smart contracts
TestEngine engine = new(true);
// Get neo token smart contract and mock balanceOf to always return 123
var neo = engine.FromHash<NeoToken>(engine.Native.NEO.Hash,
mock => mock.Setup(o => o.BalanceOf(It.IsAny<UInt160>())).Returns(123),
false);
// Test direct call
Assert.AreEqual(123, neo.BalanceOf(engine.ValidatorsAddress));
// Test vm call
using (ScriptBuilder script = new())
{
script.EmitDynamicCall(neo.Hash, "balanceOf", engine.ValidatorsAddress);
Assert.AreEqual(123, engine.Execute(script.ToArray()).GetInteger());
}
It is possible to check the fee being used between multiple calls using the FeeWatcher class. To do this you can call the CreateFeeWatcher method of TestEngine or directly use the FeeConsumed property.
using var fee = engine.CreateFeeWatcher();
{
Assert.AreEqual("GAS", engine.Native.GAS.Symbol);
Assert.AreEqual(984060L, fee);
}
To fake signatures and allow testing our contracts in authorized and unauthorized environments, it's enough to replace the signers of the Transaction object in our TestEngine. This way, we can simulate the signatures of other users. It's worth noting that it's not necessary to modify the Witnesses since it's not checked whether the transaction is well-formed.
// Initialize out TestEngine
var engine = new TestEngine(true);
// Check initial value of getRegisterPrice
Assert.AreEqual(100000000000, engine.Native.NEO.RegisterPrice);
// Fake Committee Signature
engine.SetTransactionSigners(new Network.P2P.Payloads.Signer()
{
Account = engine.CommitteeAddress,
Scopes = Network.P2P.Payloads.WitnessScope.Global
});
// Change RegisterPrice to 123
engine.Native.NEO.RegisterPrice = 123;
Assert.AreEqual(123, engine.Native.NEO.RegisterPrice);
// Now test it without this signature
engine.SetTransactionSigners(TestEngine.GetNewSigner());
Assert.ThrowsException<TargetInvocationException>(() => engine.Native.NEO.RegisterPrice = 123);
Testing that our events have been triggered has never been so easy. Simply when a contract notification is launched, the corresponding event will be invoked, making it easier to capture and detect.
// Create and initialize TestEngine
var engine = new TestEngine(true);
// Fake signature of ValidatorsAddress
engine.SetTransactionSigners(new Network.P2P.Payloads.Signer()
{
Account = engine.ValidatorsAddress,
Scopes = Network.P2P.Payloads.WitnessScope.Global
});
// Define address to transfer funds
UInt160 addressTo = UInt160.Parse("0x1230000000000000000000000000000000000000");
// Attach to Transfer event
var raisedEvent = false;
engine.Native.NEO.OnTransfer += (UInt160 from, UInt160 to, BigInteger amount) =>
{
Assert.AreEqual(engine.Transaction.Sender, from);
Assert.AreEqual(addressTo, to);
Assert.AreEqual(123, amount);
// If the event is raised, the variable will be changed
raisedEvent = true;
};
Assert.AreEqual(0, engine.Native.NEO.BalanceOf(addressTo));
// Transfer funds
Assert.IsTrue(engine.Native.NEO.Transfer(engine.Transaction.Sender, addressTo, 123, null));
// Ensure that we have balance and the event was raised
Assert.IsTrue(raisedEvent);
Assert.AreEqual(123, engine.Native.NEO.BalanceOf(addressTo));
To calculate the coverage of a contract, it is enough to call the GetCoverage method of our TestEngine, it require the EnableCoverageCapture property of the TestEngine to be enabled.
var engine = new TestEngine(true);
// Get NEO Coverage (NULL)
Assert.IsNull(engine.GetCoverage(engine.Native.NEO));
// Call NEO.TotalSupply
Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply);
// Check that the 3 instructions has been covered
Assert.AreEqual(3, engine.GetCoverage(engine.Native.NEO)?.CoveredInstructions);
Assert.AreEqual(3, engine.GetCoverage(engine.Native.NEO)?.HitsInstructions);
It is also possible to call it to obtain the specific coverage of a method, either through an expression or manually.
var engine = new TestEngine(true);
// Call NEO.TotalSupply
Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply);
// Oracle was not called
var methodCovered = engine.GetCoverage(engine.Native.Oracle, o => o.Finish());
Assert.IsNull(methodCovered);
// NEO.TotalSupply is covered
methodCovered = engine.GetCoverage(engine.Native.NEO, o => o.TotalSupply);
Assert.AreEqual(3, methodCovered?.TotalInstructions);
Assert.AreEqual(3, methodCovered?.CoveredInstructions);
// Check coverage by raw method
methodCovered = engine.GetCoverage(engine.Native.Oracle, "finish", 0);
Assert.IsNull(methodCovered);
methodCovered = engine.GetCoverage(engine.Native.NEO, "totalSupply", 0);
Assert.AreEqual(3, methodCovered?.TotalInstructions);
Assert.AreEqual(3, methodCovered?.CoveredInstructions);
Additionally, it's important to highlight that both method and contract coverages have a Dump method, through which one can obtain a text or HTML representation of the coverage.
You might be interested in adding a unit test that checks the coverage at the end of execution, you can do it as shown below:
[TestClass]
public class CoverageContractTests
{
/// <summary>
/// Required coverage to be success
/// </summary>
public static float RequiredCoverage { get; set; } = 1F;
[AssemblyCleanup]
public static void EnsureCoverage()
{
// Join here all of your Coverage sources
var coverage = Nep17ContractTests.Coverage;
coverage?.Join(OwnerContractTests.Coverage);
// Ennsure that the coverage is more than X% at the end of the tests
Assert.IsNotNull(coverage);
Console.WriteLine(coverage.Dump());
File.WriteAllText("coverage.html", coverage.Dump(Testing.Coverage.DumpFormat.Html));
Assert.IsTrue(coverage.CoveredPercentage >= RequiredCoverage, $"Coverage is less than {RequiredCoverage:P2}");
}
}
Keep in mind that the coverage is at the instruction level, but you can also get the project's coverage on the source code using the debug file (*.nefdbgnfo) generated by R3E.Compiler.CSharp. To do this, you need to compile the project with the -d or --debug argument, and set up a unit test like the following:
[TestClass]
public class CoverageContractTests
{
/// <summary>
/// Required coverage to be success
/// </summary>
public static decimal RequiredCoverage { get; set; } = 1M;
[AssemblyCleanup]
public static void EnsureCoverage()
{
// Join here all of your coverage sources
var coverage = Nep17ContractTests.Coverage;
coverage?.Join(OwnerContractTests.Coverage);
// Dump coverage to console
Assert.IsNotNull(coverage, "Coverage can't be null");
Console.WriteLine(coverage.Dump());
// Write basic instruction html coverage
File.WriteAllText("coverage.instruction.html", coverage.Dump(DumpFormat.Html));
// Load our debug file
if (NeoDebugInfo.TryLoad("templates/neocontractnep17/Artifacts/Nep17Contract.nefdbgnfo", out var dbg))
{
// Write the cobertura format
File.WriteAllText("coverage.cobertura.xml", coverage.Dump(new CoberturaFormat((coverage, dbg))));
// Write the report to the specific path
CoverageReporting.CreateReport("coverage.cobertura.xml", "./coverageReport/");
}
// Ensure that the coverage is more than X% at the end of the tests
Assert.IsTrue(coverage.CoveredLinesPercentage >= RequiredCoverage, $"Coverage is less than {RequiredCoverage:P2}");
}
}
The currently known limitations are:
FromHash knowing the hash of the contract to be created.| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.9.0-test.21178857613 | 78 | 1/20/2026 |
| 3.0.0 | 207 | 8/3/2025 |
| 2.1.0 | 173 | 8/3/2025 |
| 2.0.0 | 183 | 8/3/2025 |
| 1.0.4 | 156 | 8/2/2025 |
| 1.0.3 | 233 | 8/1/2025 |
| 1.0.3-release | 184 | 8/1/2025 |
| 1.0.3-fixed | 187 | 8/1/2025 |
| 1.0.2 | 172 | 7/19/2025 |
| 1.0.1 | 187 | 7/18/2025 |
| 1.0.0 | 213 | 7/18/2025 |
| 0.0.4 | 241 | 7/14/2025 |
| 0.0.1 | 225 | 7/13/2025 |