Cover image

How to Write File-Based JavaScript Tests With Real Files

Apr 21, 2021

Hey guys, this post is about writing tests for projects that access the file system by reading and writing files to disk.

A lot of my past projects in some way had to do with file access. I started to test with mocking libraries like mock-fs, but soon recognized that they do not work for all cases, and sometimes you are using third party libraries internally that you cannot mock easily. So I thought of a different solution and the one I'm using right now for most projects actually uses real files.

with-local-tmp-dir and output-files

Why not use real files for testing instead of mocking? I built an NPM package called with-local-tmp-dir that basically creates a temporary subfolder inside cwd, cds into it, runs a function, and cds back to the previous cwd afterwards. In this function you can create files and pretty much anything, run your unit under test. Afterwards the folder is removed and everything is cleaned up. You actually do not solely need to use it for tests, you can use it anywhere, but it's mostly useful for tests.

I also wrote another helper package output-files that creates a whole file tree at once by passing an object. It's much easier than writing a lot of fs.writeFile calls to create many files.

Let's test a scaffolding tool!

Alright, let's dive into it! First of all you need a testing framework. I'm going to use Mocha here, but you can also use Jest or any other framework of your choice. I'm also using expect for assertions. After that, we'll install some packages that we need to write our tests:

$ npm install --save-dev with-local-tmp-dir output-files fs-extra endent

We are going to test a small scaffolding tool that writes config files to disk. If a file already exists, it is not overwritten. Otherwise a default file is created. It's actually not important how it works but how we test it 😀.

Writing our first test

Let's add a test file:

// index.spec.js

const withLocalTmpDir = require('with-local-tmp-dir')
const endent = require('endent')
const expect = require('expect')
const fs = require('fs-extra')

const scaffold = require('.')

it('no existing files', () => withLocalTmpDir(async () => {
  await scaffold()
  expect(await fs.readFile('README.md', 'utf8'))
    .toEqual(endent`
      ## Package

      This is a test package.
    `)
  expect(await fs.readFile('.configrc.json', 'utf8'))
    .toEqual(endent`
      {
        "name": "Package"
      }
    `)
}))

And we can run our test via:

$ mocha index.spec.js

Pretty neat already, we have tested if the scaffolding tool creates a README.md and a .configrc.json file and checks if the contents are correct!

Writing files beforehand

Let's add another test that checks if the files are preserved if they are already existing. We are going to use output-files to write those files.

// index.spec.js

const withLocalTmpDir = require('with-local-tmp-dir')
const outputFiles = require('output-files')
const endent = require('endent')
const expect = require('expect')
const fs = require('fs-extra')

const scaffold = require('.')

it('existing files', () => withLocalTmpDir(async () => {
  await outputFiles({
    'README.md': endent`
      ## My Package

      Here is how to use this package.
    `,
    '.configrc.json': endent`
      {
        "name": "My Package"
      }
    `
  })
  await scaffold()
  expect(await fs.readFile('README.md', 'utf8'))
    .toEqual(endent`
      ## My Package

      Here is how to use this package.
    `)
  expect(await fs.readFile('.configrc.json', 'utf8'))
    .toEqual(endent`
      {
        "name": "My Package"
      }
    `)
}))

Great, that's already most of the work! Of course you can go into detail now and write more tests, but technically that's all it needs. You see, writing file-based tests with these packages is not a lot of more work than without them and you can use real files for your tests 🚀.

Writing Git-based tests

The test setup actually opens up another door: Using Git repositories for tests! I know this sounds a bit scary at first, but now that we can write files to disk for our tests, why not git init a git repository?

Let's assume that our scaffolding tool makes use of the currently checked out Git repository and puts the origin URL into the .configrc.json file. Now we can test if this works by actually instantiating a Git repository in our testing folder. We need another package for running child processes, run npm install --save-dev execa.

// index.spec.js

const withLocalTmpDir = require('with-local-tmp-dir')
const endent = require('endent')
const expect = require('expect')
const fs = require('fs-extra')
const execa = require('execa')

const scaffold = require('.')

it('uses repository url', () => withLocalTmpDir(async () => {
  await execa.command('git init')
  await execa.command('git config user.email "foo@bar.de"')
  await execa.command('git config user.name "foo"')
  await execa.command('git remote add origin git@github.com:foo/bar.git')
  await scaffold()
  expect(await fs.readFile('README.md', 'utf8'))
    .toEqual(endent`
      ## Package

      This is a test package.
    `)
  expect(await fs.readFile('.configrc.json', 'utf8'))
    .toEqual(endent`
      {
        "name": "Package",
        "repo": "git@github.com:foo/bar.git"
      }
    `)
}))

Be cautious though, if the repository is not initialized correctly, user user Git config might be overridden.

Conclusion

You see there are plenty of possibilities! 🥳 What do you think about this? Let me know in the comments! Also, if you like with-local-tmp-dir and output-files, give it a star on GitHub 🌟.

If you like what I'm doing, follow me on Twitter or check out my website. Also consider donating at Buy Me a Coffee, PayPal or Patreon. Thank you so much! ❤️

Subscribe via RSS