Skip to main content

· 2 min read
Jesse Mitchell

Hey there!

This release introduces significant improvements to Pineapple to make development & testing a better experience for all.

Support for Bun

Pineapple's internals have been reworked to make it possible to use Bun as the test runner, which has some significant advantages:

  • It transpiles every file for you, making it simpler to test your TypeScript, JSX and Flow projects.
  • Runs on a modified version of JavaScriptCore, which in some cases runs faster than V8.
  • Has a quicker cold-start time, which is useful for continuous testing.

If you pass in the --bun flag while invoking Pineapple, the framework will opt to use it over the traditional Node.js runner.

Caveat Emptor: If you're testing projects that depend heavily on Node-specific APIs, Bun may not be the ideal runner for your use-case.

Continuous Testing

Prior to this release, Pineapple was a one-shot test runner; you'd invoke the program & it'd spit out test results.

While it was certainly possible to pair Pineapple with nodemon or chokidar CLI, this would likely run every test in your project, rather than just the ones you were affecting.

Using the --watch-mode or -w flag, you can run Pineapple in continuous testing mode, which will only run tests that could be impacted by your modifications. The runner will traverse the dependency chain & deduce which tests in files downstream need to be run.

An example of the snapshot functionality where the code is modified and the snapshot fails due to a renamed attribute

Video

· One min read
Jesse Mitchell

Hey everyone!

This release is focused on providing some additional small quality of life improvements to the tool.

Better Snapshots

Prior to this release, all snapshots were captured in a global pineapple-snapshot, and while this worked, I don't believe it made reviewing particularly effective.

When snapshots are captured, it will now save in close proximity to your file that you're testing, appending a .psnap to the file name.

So if you were testing a ./src/math.js file, the snapshot will be persisted to ./src/math.js.psnap

More Hooks

This version introduces a few new hooks for test lifecycle management.

You may now use:

  • @beforeGlobal
  • @beforeEachGlobal
  • @afterGlobal
  • @afterEachGlobal

These hooks were introduced to make it easier to pair Pineapple with measurement frameworks, where you might need to reset certain fields.

Right now, the functions invoked do not receive any arguments, but this will likely be addressed in a future version.

Better Output

In certain cases, important error feedback was suppressed by the framework, thus making it difficult to rectify issues identified by the test. This feedback should no longer be suppressed.

· 2 min read
Jesse Mitchell

Hi all!

This release is focused on providing some small quality of life improvements to the property-based testing features within Pineapple.

There are two main additions to the technology:

Namespaces

/**
* Creates the static values for use in various scenarios in our codebase.
* @pineapple_define friends
*/
function define () {
return {
kevin: { /* ... */ },
shane: { /* ... */ },
emily: { /* ... */ }
}
}

/**
* #friends.emily, #friends.shane returns 'Battle won!'
* #friends.shane, #friends.kevin returns 'Battle draw!'
*/
function fight (attacker, defender) {
/* ... */
}

Namespaces might make it simpler to set up various generators & static values that you might wish to use throughout your tests.

Better Constant Detection

When you set up definitions in Pineapple, the testing framework will do its best to try to keep track of whether your "arbitrary expression" is actually constant.

This prevents a bunch of duplicate tests from taking place, particularly when it would be annoying (like in snapshots).

Previously, when one would try the following:

/**
* @pineapple_define
*/
function define () {
return { age: 17 }
}

/**
* @test { name: 'Kevin', age: #age }
* The above would not be detected as static in v0.9.0,
* but will be in v0.9.1
*/
function setupAccount({ name, age }) {
/* ... */
}

/**
* @test #age returns false
* The above will be detected as constant in both v0.9.0 and v0.9.1
*/
function isAmericanDrinkingAge (age) {
return age >= 21
}

Pineapple would not be able to detect that the expression { name: 'Kevin', age: #age } was actually a constant expression. However, if you used #age outside of a structure as seen in the second example, it would work!

To make developer's lives easier, Pineapple has been improved to try to do a better job of detecting constant structures.

· 3 min read
Jesse Mitchell

Hi all!

This release is focused on introducing fuzzing / property based testing to the Pineapple framework, which should make it ridiculously easy to cover a variety of test cases with simple test expressions.

Utilizing the amazing fast-check npm package, Pineapple is now able to fuzz a handful of test-cases and shrink any counter-examples down to the smallest test-case it can find to trip an error.

For example:


/**
* Using fuzz testing, this will cover a handful of scenarios,
* positives, negatives, zeroes
* Without you needing to go over each example explicitly.
*
* @test #integer, #integer returns @ as number
* @test #integer, #integer returns args.0 + args.1
*
* The above test is a little silly since it's embedding the
* same logic in the test, but demonstrates that it's possible.
*/
function add (a, b) {
return a + b
}

/**
* @test #array(#integer) returns @ as number
* @test #array(#string, { minLength: 1 }) throws
* @test [1, 2, 3] returns 6
* @test [#integer, 2, 3] returns args.0.0 + 5
* @test [] returns 0
*/
export function sum (values) {
if (values.some(i => typeof i !== 'number')) throw new Error('An item in the array is not a number.')
return values.reduce((a, b) => a + b, 0)
}

/**
* @test { name: #string, age: #integer(1, 20) } throws
* @test { name: 'Jesse', age: #integer(21, 80) } returns cat(args.0.name, ' is drinking age.')
*/
export function drinkingAge ({ name, age }) {
if (age >= 21) return `${name} is drinking age.`
throw new Error(`${name} is not drinking age.`)
}

This works great for handling a variety of scenarios without having to write much code, and also works with the snapshot tech built into Pineapple (making it even easier to pin functionality for a handful of test-cases).

When your tests fail though, Pineapple & Fast-Check will work together to help identify the issue.

/**
* A simple template function.
* @test 'Hello $0' ~> #string returns cat('Hello ', args.0)
* @param {string} templateString
*/
export function template (templateString) {
/** @param {string} replace */
return replace => templateString.replace(/\$0/g, replace)
}
✖ Failed test (template): 'Hello $0' ~> #string returns cat('Hello ', args.0)
>> file:///Users/jesse/Documents/Projects/pineapple/test/fuzz.js:35
- Expected
+ Received

- Hello $$
+ Hello $
Failing Example: [
"$$"
]
Shrunk 4 times.
Seed: -2121637705

Fast-Check shrinks the test-case to help you as the developer realize: "Oh! The replace string needs escaped because the $ character is special in the replace function."

If you wish to read up more on the Fuzz Testing technology, you may do so here.

· One min read
Jesse Mitchell

Hello!

This patch introduces the ability to select your output format, which should help with editor integrations in the future.

If you use OUTPUT_FORMAT=JSON or -f json, you are able to have Pineapple output to an ndjson stream which should be more easily parsable by a program.

For reference:

✔ Passed test (fib): 1
✔ Passed test (fib): 3
✔ Passed test (fib): 10
✔ Passed test (add): 1, 2
✔ Passed test (add): '4', 3 throws
✔ Passed test (add): 1, '0' throws
✔ Passed test (add): -1, 1
✔ Passed test (add): -1, 1 to 0
✖ Failed test (add): -1, 1 to -1

Will become the following in JSON mode:

{"type":"Success","name":"fib","input":"1","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:39"}
{"type":"Success","name":"fib","input":"3","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:40"}
{"type":"Success","name":"fib","input":"10","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:41"}
{"type":"Success","name":"add","input":"1, 2","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:2"}
{"type":"Success","name":"add","input":"'4', 3 throws","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:3"}
{"type":"Success","name":"add","input":"1, '0' throws","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:4"}
{"type":"Success","name":"add","input":"-1, 1","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:5"}
{"type":"Success","name":"add","input":"-1, 1 to 0","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:6"}
{"type":"Failure","name":"add","input":"-1, 1 to -1","message":"- Expected\n+ Received\n\n- -1\n+ 0","file":"file:///Users/jesse/Documents/Projects/pineapple/test/math.js:7"

This release also introduces the ability to run a subset of tests using the --only flag.

Additionally, it adds the file name & line number to failed test output (even in pretty mode), which should make it easier to jump to your test cases.

· One min read
Jesse Mitchell

Hi all!

This minor patch improves the developer experience around snapshots by making the output readable (as opposed to the Jest Serialization mechanism that it used in previous versions).

{
"fib(1) [dRX81e0Zt9zxfAdy4cKtrrKMfyO/nvL9WF+XRAOtEB0=]": {
"value": 1,
"async": false
},
"fib(3) [KTjgP0vq5dR61BJFF+PbmmL0idLvto8mYF5cAbndz5k=]": {
"value": 2,
"async": false
},
"fib(10) [RDou6nU/Mgg9Olsl1Kd1FGLxi1Ij/V+3bw0spgCqCnY=]": {
"value": 55,
"async": false
},
"add(1, 2) [O6M1izKkUUPb7fRhfnhMZ8VxO25LxM0bS6rw/tGm5YA=]": {
"value": 3,
"async": false
},
"add(-1, 1) [hnYzkbZiJjMD0YnEHZer8Pwyyf32Pd3dus2/O70SBZk=]": {
"value": 0,
"async": false
},
"mul(3, 5) [3uLRCxaVjev70tv9IFOlLrFQMM2wYWl0A1q5WwoopjE=]": {
"value": 15,
"async": false
}
}

This should make it simpler to review snapshots for the purposes of pull-requests.

The syntax is json-like, in that it actually uses Pineapple's grammar & functions to parse it, which will make it easier to support things like dates & bigints, or other types of values later on.

{
"addAsync(5n, 3n) [X76+w3gcfI4QVFELW0Sgv2OKYXurpbbu3cu+5ki2IfM=]": {
"value": 8n,
"async": true
}
}

· One min read
Jesse Mitchell

This patch introduces a small quality of life improvement, which I felt was particularly necessary after introducing class-based testing:

Multiline Test Cases!

/**
* @test 'Jesse', 24
* ~> $.grow(3)
* ~> $.grow(2) returns 29
*
* @test 'Rick', 62
* ~> $.grow(1) returns 63
* ~> $.grow(2) returns 65
* ~> $.getName() returns 'Rick'
* ~> $.grow() returns $.age === 66
*/
export class Person {
constructor(name, age) {
this.name = name
this.age = age
}

grow(amount = 1) {
return this.age += amount
}

getName() {
return this.name
}
}

If you write a test case on multiple lines, Pineapple will now automatically concatenate it to the test case. This is not exclusive to class / higher-order function syntax.

/**
* @test {
* tenant: 'Rick',
* length: 10,
* type: 'boat'
* } resolves
*
* @test {
* tenant: 10,
* length: 'Rick',
* type: 'boat'
* } rejects
*/
export async function createLease({ tenant, length, type = 'boat' }) {
if (typeof tenant !== 'string' || typeof length !== 'number')
throw new Error('Types do not match.')
return { type, tenant, length }
}

· 5 min read
Jesse Mitchell

The Pineapple Logo

What in the world is Pineapple?

Pineapple is a test framework designed to remove the cruft from writing unit tests and creating snapshots.

/**
* Adds numbers together for the sake of demonstrating pineapple.
* @test 1, 2 returns 3
* @test '1', 2 throws "Not a number"
* @param {number} a
* @param {number} b
*/
export function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') throw new Error('Not a number')
return a + b
}

It allows you to embed a few example test-cases in your JSDocs, making it easier to focus on your code and less on defining it & expect chains.

When you omit conditions from your test cases, it'll automatically capture the result of your test & snapshot it, making it easier to preserve expected behavior in your applications, and even easier for users to find examples on how to call your code.

An example of the snapshot functionality where the code is modified and the snapshot fails due to a renamed attribute

But... why?

While there are a lot of great test frameworks out there with solid communities and support, I've found it difficult to introduce testing to certain teams due to the cruft & ergonomics involved.

Pineapple is an attempt to make it easier to write your tests, to get more people into the habit of writing them. The idea is that if it's simple (or at least less of a pain) to write a few test cases & also flesh out your documentation, more people will take the opportunity to write them.

While frameworks like Mocha are pretty nice, writing some checks idiomatically tends to lead to verbose test-cases.

const { add } = require('../../modules/math')

describe('A description of your test suite', () => {
it('Should be able to add two numbers together', () => {
assert.equals(add(1, 2), 3)
})

it('Should throw if one of the parameters is a string', () => {
expect(add(1, '2')).to.throw()
expect(add('1', 2)).to.throw()
expect(add('1', '2')).to.throw()
})

it('should be able to add negative numbers', () => {
assert.equals(add(-3, 5), 2)
})
})

vs writing

/**
* @test 1, 2 returns 3
* @test 1, '2' throws
* @test -3, 5 returns 2
*/
function add(a, b) { ... }

And sometimes the added verbosity is nice! But sometimes it'd be a little easier to be able to get to the point & provide examples of how to call your functions.

I also wanted to make it simple to perform snapshots, because in a handful of cases (particularly on functions with more complex types), I've seen people run the functions & copy-paste the output into an expect clause.

// Snapshots by default if no conditions are specified :)
/**
* @test { name: 'Jesse', term: '8mo' }
*/
export async function generateDocument({ name, term }) {
return {
name,
term,
lease: await acquireLease({ name, term })
}
}

Got slightly more complex examples?

In some cases, you may want to set up a more complex test, these are the times that .test.js files are warranted in Pineapple.

This allows you to compose a handful of test cases on the same code, while remaining true to vanilla javascript. Ideally the tests that would be created would be a function that you might find in a real-world implementation of your APIs.

/**
* @test "HelloWorld"
* @test "Hello1" returns truthy
* @test "Hello" returns truthy
* @test "th1ng$Here" returns falsy
*/
export function commonRule (pw) {
return password(
min(8),
max(16),
hasDigits(1),
hasSpecial(1),
hasLowerCase(1),
hasUpperCase(1)
)(pw)
}

Pineapple can also be used to test classes, though if you get to a point where you're needing to compose numerous scenarios across multiple entities, it might be ideal to start exploring a Scenario Testing framework like Cucumber. :)

/**
* A basic bank account class that allows for withdrawing & depositing money.
*
* @test 100
* ~> $.withdraw(10)
* ~> $.deposit(20) returns $.balance === 110
*
* @test 0
* ~> $.withdraw(100) throws 'Insufficient funds'
* ~> $.deposit(50) returns 50
* ~> $.withdraw(20) returns 30
*
* @test 100
* ~> $.withdraw(-10) throws
* ~> $.deposit(-10) throws
*/
export class Account {
constructor(balance) {
this.balance = balance
}

withdraw (amount) {
if (amount < 0) throw new Error('Amount must be greater than zero')
if (this.balance < amount) throw new Error('Insufficient funds')
return this.balance -= amount
}

deposit (amount) {
if (amount < 0) throw new Error('Amount must be greater than zero')
return this.balance += amount
}
}

Pineapple isn't meant to replace all forms of testing; just your basic unit tests & similar.

As of May 9th 2022, the project still has not had a v1.0 release, thus should still be considered experimental. There are still some edges to round out before a stable release, but the technology is functional. :)