
Security News
Risky Biz Podcast: Making Reachability Analysis Work in Real-World Codebases
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Puty 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.
!include
directivenpm install puty
Get up and running with Puty in just a few minutes!
npm install puty
Ensure your package.json
has ES modules enabled:
{
"type": "module"
}
Create utils/validator.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
:
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'
Create puty.test.js
:
import path from 'path'
import { setupTestSuiteFromYaml } from 'puty'
const __dirname = path.dirname(new URL(import.meta.url).pathname)
await setupTestSuiteFromYaml(__dirname);
npx vitest
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.
To enable automatic test reruns when YAML test files change, create a vitest.config.js
file in your project root:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
forceRerunTriggers: [
'**/*.js',
'**/*.{test,spec}.yaml',
'**/*.{test,spec}.yml'
],
},
});
This configuration ensures that Vitest will re-run your tests whenever you modify either your JavaScript source files or your YAML test files.
Here's a complete example of testing JavaScript functions with Puty:
file: './math.js'
group: math
suites: [add, increment]
---
### Add
suite: add
exportName: add
---
case: add 1 and 2
in:
- 1
- 2
out: 3
---
case: add 2 and 2
in:
- 2
- 2
out: 4
---
### Increment
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:
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:
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
mode: 'class'
- Indicates this suite tests a classconstructorArgs
- Arguments passed to the class constructorexecutions
- Array of method calls to execute in sequence
method
- 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 call
user.profile.name
)settings.getTheme
)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:
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:
Key behaviors:
out
field is omitted: The function is called but its return value is not assertedout:
is present (even empty): The return value is asserted (empty value in YAML equals null
)executions
Examples:
# No assertion on return value
case: test without return assertion
in: [1, 2]
# Assert return value is null
case: test null return
in: [1, 2]
out:
# Assert return value is 42
case: test specific return
in: [1, 2]
out: 42
To assert that a function returns undefined
, use the special keyword __undefined__
:
# 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:
out: __undefined__
)value: __undefined__
)You can test that functions or methods throw expected errors:
case: divide by zero
in: [10, 0]
throws: "Division by zero"
Puty supports mocking dependencies using the $mock:
syntax. This is useful for testing functions that have external dependencies like loggers, API clients, or callbacks.
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']
Mocks can be defined at three levels (case overrides suite, suite overrides global):
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' }
Mocks are perfect for testing event-driven code:
case: test event emitter
executions:
- method: on
in: ['data', $mock:callback]
- method: emit
in: ['data', 'hello']
mocks:
callback:
calls:
- in: ['hello']
Puty supports the !include
directive to modularize and reuse YAML test files. This is useful for:
You can include entire YAML documents:
file: "./math.js"
group: math-tests
suites: [add]
---
!include ./suite-definition.yaml
---
!include ./test-cases.yaml
You can also include specific values within a YAML document:
case: test with shared data
in: !include ./test-data/input.yaml
out: !include ./test-data/expected-output.yaml
The !include
directive supports recursive includes, allowing included files to include other files:
# main.yaml
!include ./level1.yaml
# level1.yaml
suite: test
---
!include ./level2.yaml
# level2.yaml
case: nested test
in: []
out: "success"
!include
are relative to the YAML file containing the directivePuty test files use multi-document YAML format with three types of documents:
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
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
For function tests:
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:
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
Puty supports accessing nested properties and calling nested methods using dot notation:
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'
FAQs
A tooling function to test javascript functions and classes.
The npm package puty receives a total of 119 weekly downloads. As such, puty popularity was classified as not popular.
We found that puty demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
This episode explores the hard problem of reachability analysis, from static analysis limits to handling dynamic languages and massive dependency trees.
Security News
/Research
Malicious Nx npm versions stole secrets and wallet info using AI CLI tools; Socket’s AI scanner detected the supply chain attack and flagged the malware.
Security News
CISA’s 2025 draft SBOM guidance adds new fields like hashes, licenses, and tool metadata to make software inventories more actionable.