Testing
Testing N1 Applications
This guide explains how to write tests for your N1 applications using the built-in client testing system.
Overview
N1 provides a simple testing system that allows you to test your applications using the same client interface that external users would use. No additional testing libraries are required.
Tests in N1 are simple TypeScript functions that use the NAppClient to interact with your application, just like a real user would.
Basic Test Structure
Create Test File
Create a test file that exports a default async function:
import { NAppClient } from "@n1xyz/nts-sdk";
import { ContractIDL } from "../build/idl";
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
await client.actions.init();
await client.actions.increment(2);
const count = await client.state.count();
console.log('Current count:', count);
}
Define Your App
Your application code:
import { NApp, createExecutableFunctions } from '@n1xyz/nts-compiler';
class Counter extends NApp {
count: number;
init(): void {
this.count = 0;
}
increment(amount: number): void {
this.count += amount;
}
decrement(amount: number): void {
this.count -= amount;
}
}
export const { init, increment, decrement } = createExecutableFunctions(Counter);
Build and Test
The build system generates the IDL and provides the client interface for testing.
Development Mode Testing
When using the development mode (n1 app dev
), the CLI provides an automated test environment that makes it easy to iterate on your tests.
How it works
When you run tests in development mode, for each test file the CLI will:
- Deploy a fresh instance of your app with a unique ID
- Run the test file with the deployed app
- Clean up by removing the test app after completion
Test Context
Your test files should export a function that accepts a context object containing:
client
- An initialized NAppClient instanceappId
- The ID of the deployed appidl
- The app’s interface definitionwalletPubKey
- The public key of your wallet
Example test file:
export default async function(context) {
const { client, appId } = context;
// Your test code here
const result = await client.someAction();
assert(result.success);
}
The development mode automatically cleans up test deployments after each test run, so you don’t need to worry about lingering test apps.
Writing Tests
Test Actions
Use client.actions
to call your application’s transaction actions:
import { NAppClient } from "@n1xyz/nts-sdk";
import { ContractIDL } from "../build/idl";
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
// Initialize the app
await client.actions.init();
// Call increment action
await client.actions.increment(5);
// Call decrement action
await client.actions.decrement(2);
// Read final state
const finalCount = await client.state.count();
console.log('Final count:', finalCount); // Should be 3
}
Test State Reading
Use client.state
to read your application’s state:
import { NAppClient } from "@n1xyz/nts-sdk";
import { ContractIDL } from "../build/idl";
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
// Initialize app
await client.actions.init();
// Check initial state
const initialCount = await client.state.count();
console.log('Initial count:', initialCount); // Should be 0
// Perform operations
await client.actions.increment(10);
await client.actions.decrement(3);
// Check final state
const finalCount = await client.state.count();
console.log('Final count:', finalCount); // Should be 7
}
Testing Complex Applications
Token Application Test
import { NAppClient } from "@n1xyz/nts-sdk";
import { ContractIDL } from "../build/idl";
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
// Initialize the token app
await client.actions.init();
// Create a token
await client.actions.createToken(
'1000000', // Total supply
'My Game Token' // Metadata
);
// Transfer tokens to another user
await client.actions.transferTokens(
'recipient_address',
'mint_id_from_creation',
'1000'
);
// Check app state
const totalSupply = await client.state.totalSupply();
console.log('Total supply:', totalSupply);
const userCount = await client.state.userCount();
console.log('User count:', userCount);
}
Multi-User Testing
import { NAppClient } from "@n1xyz/nts-sdk";
import { ContractIDL } from "../build/idl";
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
// Test with different signers
await client.actions.init();
// Register first user
await client.actions.registerUser('Alice', 'alice@example.com');
// Check user registration
const userCount = await client.state.userCount();
console.log('Users registered:', userCount);
// Additional operations...
await client.actions.updateUserProfile('Alice Updated');
// Verify state changes
const isActive = await client.state.isActive();
console.log('App is active:', isActive);
}
Testing Patterns
Error Handling Tests
import { NAppClient } from "@n1xyz/nts-sdk";
import { ContractIDL } from "../build/idl";
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
await client.actions.init();
try {
// This should fail
await client.actions.transferTokens('', 'invalid_mint', '0');
console.log('ERROR: Should have thrown an error');
} catch (error) {
console.log('✅ Correctly caught error:', error.message);
}
try {
// This should also fail
await client.actions.increment(-5); // Negative increment
console.log('ERROR: Should have thrown an error');
} catch (error) {
console.log('✅ Correctly caught error:', error.message);
}
}
Validation Tests
import { NAppClient } from "@n1xyz/nts-sdk";
import { ContractIDL } from "../build/idl";
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
await client.actions.init();
// Test valid operations
await client.actions.setMessage('Valid message');
const message = await client.state.message();
console.log('✅ Valid message set:', message);
// Test edge cases
await client.actions.setMessage('A'.repeat(1000)); // Long message
const longMessage = await client.state.message();
console.log('✅ Long message length:', longMessage.length);
// Test boundary conditions
await client.actions.increment(Number.MAX_SAFE_INTEGER - 1000);
const largeCount = await client.state.count();
console.log('✅ Large count handled:', largeCount);
}
Test Organization
Comprehensive Test Suite
import { NAppClient } from "@n1xyz/nts-sdk";
import { ContractIDL } from "../build/idl";
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
console.log('🧪 Starting comprehensive test suite...\n');
// Test 1: Initialization
console.log('📝 Test 1: Initialization');
await client.actions.init();
const initialState = await client.state.count();
console.log(`✅ Initial count: ${initialState}\n`);
// Test 2: Basic Operations
console.log('📝 Test 2: Basic Operations');
await client.actions.increment(10);
await client.actions.decrement(3);
const afterOps = await client.state.count();
console.log(`✅ After operations: ${afterOps}\n`);
// Test 3: Edge Cases
console.log('📝 Test 3: Edge Cases');
await client.actions.increment(0); // Zero increment
const afterZero = await client.state.count();
console.log(`✅ After zero increment: ${afterZero}\n`);
// Test 4: State Consistency
console.log('📝 Test 4: State Consistency');
const stateSnapshot = await client.state.count();
await client.actions.increment(5);
await client.actions.decrement(5);
const stateAfterRoundTrip = await client.state.count();
if (stateSnapshot === stateAfterRoundTrip) {
console.log('✅ State consistency maintained\n');
} else {
console.log('❌ State consistency failed\n');
}
console.log('🎉 Test suite completed!');
}
Best Practices
1. Clear Test Flow
// ✅ Good: Clear test flow with logging
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
console.log('Starting test: Token Creation');
await client.actions.init();
console.log('✅ App initialized');
await client.actions.createToken('1000', 'Test Token');
console.log('✅ Token created');
const supply = await client.state.totalSupply();
console.log(`✅ Total supply: ${supply}`);
}
2. Test Real Scenarios
// ✅ Good: Test realistic user flows
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
// Simulate real user registration flow
await client.actions.init();
await client.actions.registerUser('John Doe', 'john@example.com');
await client.actions.updateUserProfile('Updated John Doe');
// Verify the complete flow worked
const userCount = await client.state.userCount();
const isActive = await client.state.isActive();
console.log(`Users: ${userCount}, Active: ${isActive}`);
}
3. Test Error Conditions
// ✅ Good: Test error conditions
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
await client.actions.init();
// Test various error conditions
const errorTests = [
() => client.actions.transferTokens('', 'invalid', '0'),
() => client.actions.registerUser('', ''),
() => client.actions.increment(-1)
];
for (const test of errorTests) {
try {
await test();
console.log('❌ Should have thrown error');
} catch (error) {
console.log('✅ Error correctly handled:', error.message);
}
}
}
4. Use Descriptive Logging
// ✅ Good: Descriptive logging for debugging
export default async function ({ client }: { client: NAppClient<ContractIDL> }) {
console.log('🚀 Testing message system functionality');
await client.actions.init();
console.log('📋 App initialized successfully');
await client.actions.setMessage('Hello, N1!');
console.log('💬 Message set successfully');
const message = await client.state.message();
console.log(`📖 Retrieved message: "${message}"`);
console.log('✨ Test completed successfully');
}
Running Tests
Tests are executed as part of your N1 application’s build and deployment process. The testing system automatically:
- Compiles your application code
- Generates the IDL interface
- Creates a test client
- Executes your test functions
- Reports results
Remember that tests run against your actual application logic, so they validate the real behavior that users will experience.