A tooling function to test javascript functions and classes.
npm install putyPuty is a declarative testing framework that allows you to write unit tests using YAML files instead of JavaScript code. It's built on top of Vitest and designed to make testing more accessible and maintainable by separating test data from test logic.
Puty is ideal for testing pure functions - functions that always return the same output for the same input and have no side effects. The declarative YAML format perfectly captures the essence of pure function testing: given these inputs, expect this output.
- Features
- Installation
- Quick Start
- Usage
- Testing Functions
- Testing Classes
- Testing Factory Functions
- Error Testing
- Using Mocks
- Using !include Directive
- YAML Structure
- ๐ Write tests in simple YAML format
- ๐ Zero-config Vitest plugin
- ๐ฆ Modular test organization with !include directive
- ๐ฏ Clear separation of test data and test logic
- ๐งช Mock support for testing functions with dependencies
- โก Powered by Vitest for fast test execution
``bash`
npm install puty
Get up and running with Puty in just a few minutes!
- Node.js with ES modules support
- Vitest installed in your project
`bash`
npm install puty vitest
Create vitest.config.js:
`js
import { defineConfig } from 'vitest/config'
import { putyPlugin } from 'puty/vitest'
export default defineConfig({
plugins: [putyPlugin()]
})
`
Create utils/validator.js:
`js
export function isValidEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
`
Create validator.test.yaml:
`yaml`
file: './utils/validator.js'
group: validator
suites: [isValidEmail, capitalize]
---
suite: isValidEmail
exportName: isValidEmail
---
case: valid email should return true
in: ['user@example.com']
out: true
---
case: invalid email should return false
in: ['invalid-email']
out: false
---
case: empty string should return false
in: ['']
out: false
---
suite: capitalize
exportName: capitalize
---
case: capitalize first letter
in: ['hello']
out: 'Hello'
---
case: single letter
in: ['a']
out: 'A'
Add a test script to your package.json:
`json`
{
"scripts": {
"test": "vitest run"
}
}
`bash`
npm test
You should see output like:
``
โ validator > isValidEmail > valid email should return true
โ validator > isValidEmail > invalid email should return false
โ validator > isValidEmail > empty string should return false
โ validator > capitalize > capitalize first letter
โ validator > capitalize > single letter
๐ That's it! You've just created declarative tests using YAML instead of JavaScript.
Here's a complete example of testing JavaScript functions with Puty:
`yaml`
file: './math.js'
group: math
suites: [add, increment]
---$3
suite: add
exportName: add
---
case: add 1 and 2
in:
- 1
- 2
out: 3
---
case: add 2 and 2
in:
- 2
- 2
out: 4
---$3
suite: increment
exportName: default
---
case: increment 1
in:
- 1
out: 2
---
case: increment 2
in:
- 2
out: 3
This under the hood creates a test structure in Vitest like:
`js`
describe('math', () => {
describe('add', () => {
it('add 1 and 2', () => { ... })
it('add 2 and 2', () => { ... })
})
describe('increment', () => {
it('increment 1', () => { ... })
it('increment 2', () => { ... })
})
})
See the YAML Structure section for detailed documentation of all available fields.
Puty also supports testing classes with method calls and state assertions:
`yaml`
file: './calculator.js'
group: Calculator
suites: [basic-operations]
---
suite: basic-operations
mode: 'class'
exportName: default
constructorArgs: [10] # Initial value
---
case: add and multiply operations
executions:
- method: add
in: [5]
out: 15
asserts:
- property: value
op: eq
value: 15
- method: multiply
in: [2]
out: 30
asserts:
- property: value
op: eq
value: 30
- method: getValue
in: []
out: 30
#### Class Test Structure
- mode: 'class' - Indicates this suite tests a classconstructorArgs
- - Arguments passed to the class constructorexecutions
- - Array of method calls to execute in sequencemethod
- - Name of the method to call (supports nested: user.api.getData)in
- - Arguments to pass to the methodout
- - Expected return value (optional)asserts
- - Assertions to run after the method calluser.profile.name
- Property assertions: Check instance properties (supports nested: )settings.getTheme
- Method assertions: Call methods and check their return values (supports nested: )
Puty supports testing factory functions that return objects with methods. When using executions in a function test, you can omit the out field to skip asserting the factory's return value:
`yaml`
file: './store.js'
group: store
---
suite: createStore
exportName: createStore
---
case: test store methods
in:
- { count: 0 }No 'out' field - skip return value assertion
executions:
- method: getCount
in: []
out: 0
- method: dispatch
in: [{ type: 'INCREMENT' }]
out: 1
- method: getCount
in: []
out: 1
This pattern is useful for:
- Factory functions that return objects with methods
- Builder patterns
- Module patterns that return APIs
- Any function that returns an object you want to test methods on
Key behaviors:
- When out field is omitted: The function is called but its return value is not assertedout:
- When is present (even empty): The return value is asserted (empty value in YAML equals null)executions
- This works for any function test, with or without
Examples:
`yamlNo assertion on return value
case: test without return assertion
in: [1, 2]
#### Testing Undefined Values
To assert that a function returns
undefined, use the special keyword __undefined__:`yaml
Assert function returns undefined
case: test undefined return
in: []
out: __undefined__Also works in executions
executions:
- method: doSomething
in: []
out: __undefined__
And in mock definitions
mocks:
callback:
calls:
- in: ['data']
out: __undefined__
`The
__undefined__ keyword works in:
- Function return value assertions (out: __undefined__)
- Method return value assertions in executions
- Mock return values
- Mock input expectations
- Property assertions (value: __undefined__)$3
You can test that functions or methods throw expected errors:
`yaml
case: divide by zero
in: [10, 0]
throws: "Division by zero"
`$3
Puty supports mocking dependencies using the
$mock: syntax. This is useful for testing functions that have external dependencies like loggers, API clients, or callbacks.#### Basic Mock Example
`yaml
file: './calculator.js'
group: calculator
---
suite: calculate
exportName: calculateWithLogger
---
case: test with mock logger
in:
- 10
- 5
- $mock:logger
out: 15
mocks:
logger:
calls:
- in: ['Calculating 10 + 5']
- in: ['Result: 15']
`#### Mock Hierarchy
Mocks can be defined at three levels (case overrides suite, suite overrides global):
`yaml
file: './service.js'
group: service
mocks:
globalApi: # Global mock - available to all suites
calls:
- in: ['/default']
out: { status: 200 }
---
suite: userService
mocks:
api: # Suite mock - available to all cases in this suite
calls:
- in: ['/users']
out: { users: [] }
---
case: get user with mock
in: [123, $mock:api]
out: { id: 123, name: 'John' }
mocks:
api: # Case mock - overrides suite mock
calls:
- in: ['/users/123']
out: { id: 123, name: 'John' }
`#### Testing Callbacks
Mocks are perfect for testing event-driven code:
`yaml
case: test event emitter
executions:
- method: on
in: ['data', $mock:callback]
- method: emit
in: ['data', 'hello']
mocks:
callback:
calls:
- in: ['hello']
`$3
Puty supports the
!include directive to modularize and reuse YAML test files. This is useful for:
- Sharing common test data across multiple test files
- Organizing large test suites into smaller, manageable files
- Reusing test cases for different modules#### Basic Usage
You can include entire YAML documents:
`yaml
file: "./math.js"
group: math-tests
suites: [add]
---
!include ./suite-definition.yaml
---
!include ./test-cases.yaml
`#### Including Values
You can also include specific values within a YAML document:
`yaml
case: test with shared data
in: !include ./test-data/input.yaml
out: !include ./test-data/expected-output.yaml
`#### Recursive Includes
The
!include directive supports recursive includes, allowing included files to include other files:`yaml
main.yaml
!include ./level1.yamllevel1.yaml
suite: test
---
!include ./level2.yamllevel2.yaml
case: nested test
in: []
out: "success"
`#### Important Notes
- File paths in
!include are relative to the YAML file containing the directive
- Circular dependencies are detected and will cause an error
- Missing include files will result in a clear error message
- Both single documents and multi-document YAML files can be included
YAML Structure
Puty test files use multi-document YAML format with three types of documents:
$3
`yaml
file: './module.js' # Required: Path to JS file (relative to YAML file)
group: 'test-group' # Required: Test group name (or use 'name')
suites: ['suite1', 'suite2'] # Optional: List of suites to define
mocks: # Optional: Global mocks available to all suites
mockName:
calls:
- in: [args]
out: result
`$3
`yaml
suite: 'suiteName' # Required: Suite name
exportName: 'functionName' # Optional: Export to test (defaults to suite name or 'default')
mode: 'class' # Optional: Set to 'class' for class testing
constructorArgs: [arg1] # Optional: Arguments for class constructor (class mode only)
mocks: # Optional: Suite-level mocks for all cases in this suite
mockName:
calls:
- in: [args]
out: result
`$3
For function tests:
`yaml
case: 'test description' # Required: Test case name
in: [arg1, arg2] # Required: Input arguments (use $mock:name for mocks)
out: expectedValue # Optional: Expected output (omit if testing for errors)
throws: 'Error message' # Optional: Expected error message
mocks: # Optional: Case-specific mocks
mockName:
calls: # Array of expected calls
- in: [args] # Expected arguments
out: result # Optional: Return value
throws: 'error' # Optional: Throw error instead
`For class tests:
`yaml
case: 'test description'
executions:
- method: 'methodName' # Supports nested: 'user.api.getData'
in: [arg1]
out: expectedValue # Optional
throws: 'Error msg' # Optional
asserts:
- property: 'prop' # Supports nested: 'user.profile.name'
op: 'eq' # Currently only 'eq' is supported
value: expected
- method: 'getter' # Supports nested: 'settings.ui.getTheme'
in: []
out: expected
mocks: # Optional: Mocks for the entire test case
mockName:
calls:
- in: [args]
out: result
`#### Nested Properties and Methods
Puty supports accessing nested properties and calling nested methods using dot notation:
`yaml
case: 'test nested access'
executions:
- method: 'settings.ui.setTheme' # Call nested method
in: ['dark']
out: 'dark'
asserts:
- property: 'user.profile.name' # Access nested property
op: eq
value: 'John Doe'
- property: 'user.account.balance' # Deep nested property
op: eq
value: 100.50
- method: 'api.client.get' # Call nested method
in: ['/users/123']
out: 'GET /users/123'
``