Skip to content

Commit

Permalink
Snapshot functionality
Browse files Browse the repository at this point in the history
Summary:
This commit introduce a new matcher to `jest`:
`toMatchSnapshot`.
this new matcher can be called as usual:
`expect(something).toMatchSnapshot()`
the main idea is that the first time the test run it will store whatever is inside expect in a snapshot file,
the next time `jest` runs it will check against it to see if something has changed.

In order to store something on file we need to serialize it, so I currently implemented two different serialization strategies, if we are _expecting_ a `React` element to match a snapshot representation then it will be _currently_ be serialized using the `React.renderToString` method, otherwise it fallbacks to `jest`'s pretty printer.
I choose it because it provides out of the box support for `Maps` and `Sets` other than `Object`s and `Array`s.
As the title mention this is a work in progress as I plan to iterate on it to improve the serialization strategies and possible the output when it fails the expectation.

The matcher uses jasmine `Spec` `getFullName` as the
Closes #1000

Differential Revision: D3341276

fbshipit-source-id: ffff2326c104f26b612fbc4bc6fd2b7dd22f2a31
  • Loading branch information
kentaromiura authored and Facebook Github Bot 6 committed May 25, 2016
1 parent 633fc6c commit dc7f3a9
Show file tree
Hide file tree
Showing 14 changed files with 462 additions and 1 deletion.
39 changes: 39 additions & 0 deletions integration_tests/__tests__/snapshot-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails oncall+jsinfra
*/
'use strict';
const fs = require('fs');
const path = require('path');
const runJest = require('../runJest');

describe('Snapshot', () => {
it('works as expected', () => {
const result = runJest.json('snapshot', []);
const json = result.json;

expect(json.numTotalTests).toBe(2);
expect(json.numPassedTests).toBe(2);
expect(json.numFailedTests).toBe(0);
expect(json.numPendingTests).toBe(0);
expect(result.status).toBe(0);

const content = fs.readFileSync(
path.resolve(
__dirname,
'../snapshot/__tests__/__snapshots__/snapshot.js.snap'
)
);

const output = JSON.parse(content);
expect(
output['snapshot is not influenced by previous counter 0']
).not.toBe(undefined);

});
});
34 changes: 34 additions & 0 deletions integration_tests/snapshot/__tests__/snapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails oncall+jsinfra
*/
'use strict';

describe('snapshot', () => {

it('works with plain objects', () => {
const test = {
a: 1,
b: '2',
c: 'three',
};
expect(JSON.stringify(test)).toMatchSnapshot();
test.d = '4';
expect(JSON.stringify(test)).toMatchSnapshot();
});

it('is not influenced by previous counter', () => {
const test = {
a:43,
b:'43',
c:'fourtythree',
};
expect(JSON.stringify(test)).toMatchSnapshot();
});

});
1 change: 1 addition & 0 deletions integration_tests/snapshot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
8 changes: 8 additions & 0 deletions packages/jest-cli/src/cli/processArgs.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ function processArgs() {
),
type: 'boolean',
},
updateSnapshot: {
alias: 'u',
default: false,
description: _wrapDesc(
'Use this flag to re-record snapshots.'
),
type: 'boolean',
},
})
.check(argv => {
if (argv.runInBand && argv.hasOwnProperty('maxWorkers')) {
Expand Down
1 change: 1 addition & 0 deletions packages/jest-cli/src/config/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ function normalize(config, argv) {
case 'noStackTrace':
case 'persistModuleRegistryBetweenSpecs':
case 'rootDir':
case 'updateSnapshot':
case 'testEnvData':
case 'testEnvironment':
case 'testPathPattern':
Expand Down
3 changes: 3 additions & 0 deletions packages/jest-cli/src/config/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ function readConfig(argv, packageRoot) {
config.setupTestFrameworkScriptFile = argv.setupTestFrameworkScriptFile;
}

if (argv.updateSnapshot) {
config.updateSnapshot = argv.updateSnapshot;
}
config.noStackTrace = argv.noStackTrace;

return config;
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-jasmine2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"main": "src/index.js",
"dependencies": {
"graceful-fs": "^4.1.3",
"jest-util": "^12.1.0"
"jest-util": "^12.1.0",
"jest-snapshot": "^12.1.0"
},
"jest": {
"testEnvironment": "node"
Expand Down
14 changes: 14 additions & 0 deletions packages/jest-jasmine2/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

const fs = require('graceful-fs');
const jasminePit = require('./jasmine-pit');
const snapshot = require('jest-snapshot');
const JasmineReporter = require('./reporter');

const CALL_PRINT_LIMIT = 3;
Expand Down Expand Up @@ -41,6 +42,7 @@ function getActualCalls(reporter, calls, limit) {
function jasmine2(config, environment, moduleLoader, testPath) {
let env;
let jasmine;

const reporter = new JasmineReporter({
noHighlight: config.noHighlight,
noStackTrace: config.noStackTrace,
Expand Down Expand Up @@ -81,6 +83,8 @@ function jasmine2(config, environment, moduleLoader, testPath) {
};

env = jasmine.getEnv();
env.snapshotState = snapshot.getSnapshotState(jasmine, testPath);

const jasmineInterface = requireJasmine.interface(jasmine, env);
Object.assign(environment.global, jasmineInterface);
env.addReporter(jasmineInterface.jsApiReporter);
Expand Down Expand Up @@ -130,6 +134,14 @@ function jasmine2(config, environment, moduleLoader, testPath) {

env.beforeEach(() => {
jasmine.addCustomEqualityTester(iterableEquality);
jasmine.addMatchers(
snapshot.getMatchers(
testPath,
config,
jasmine,
env.snapshotState
)
);

jasmine.addMatchers({
toBeCalled: () => ({
Expand Down Expand Up @@ -251,6 +263,8 @@ function jasmine2(config, environment, moduleLoader, testPath) {
env.addReporter(reporter);
moduleLoader.requireModule(testPath);
env.execute();
env.snapshotState.snapshot.save();

return reporter.getResults();
}

Expand Down
19 changes: 19 additions & 0 deletions packages/jest-snapshot/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "jest-snapshot",
"version": "12.1.0",
"repository": {
"type": "git",
"url": "https://github.com/facebook/jest.git"
},
"license": "BSD-3-Clause",
"main": "src/index.js",
"dependencies": {
"jest-util": "^12.1.0"
},
"scripts": {
"test": "../jest-cli/bin/jest.js"
},
"jest": {
"testEnvironment": "node"
}
}
82 changes: 82 additions & 0 deletions packages/jest-snapshot/src/SnapshotFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';

const fs = require('fs');
const path = require('path');
const createDirectory = require('jest-util').createDirectory;

const SNAPSHOT_EXTENSION = '.snap';
const ensureDirectoryExists = filePath => {
try {
createDirectory(path.join(path.dirname(filePath)));
} catch (e) {}
};
const fileExists = filePath => {
try {
fs.accessSync(filePath, fs.R_OK);
return true;
} catch (e) {}
return false;
};

class SnapshotFile {

constructor(filename) {
this._filename = filename;
if (this.fileExists(filename)) {
this._content = JSON.parse(fs.readFileSync(filename));
} else {
this._content = {};
}

return this._loaded;
}

fileExists() {
return fileExists(this._filename);
}

save() {
const serialized = JSON.stringify(this._content);
if (serialized !== '{}') {
ensureDirectoryExists(this._filename);
fs.writeFileSync(this._filename, serialized);
}
}

has(key) {
return this._content[key] !== undefined;
}

get(key) {
return this._content[key];
}

matches(key, value) {
return this.get(key) === value;
}

add(key, value) {
this._content[key] = value;
}

}

module.exports = {
forFile(testPath) {
const snapshotsPath = path.join(path.dirname(testPath), '__snapshots__');

const snapshotFilename = path.join(
snapshotsPath,
path.basename(testPath) + SNAPSHOT_EXTENSION
);

return new SnapshotFile(snapshotFilename);
},
};
99 changes: 99 additions & 0 deletions packages/jest-snapshot/src/__tests__/SnapShotFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails oncall+jsinfra
*/
'use strict';

let accessShouldThrow = false;

jest
.disableAutomock()
.mock('mkdirp', () => ({sync: jest.fn()}))
.mock('fs', () => ({
accessSync: jest.fn(() => {
if (accessShouldThrow) {
throw new Error();
}
return true;
}),
readFileSync: jest.fn(fileName => {
const EXPECTED_FILE_NAME = '/foo/__tests__/__snapshots__/baz.js.snap';
expect(fileName).toBe(EXPECTED_FILE_NAME);
return '{}';
}),
writeFileSync: jest.fn((path, content) => {
expect(content).toBe('{"foo":"bar"}');
}),
}));

const TEST_FILE = '/foo/__tests__/baz.js';
const SNAPSHOT = 'foo';
const SNAPSHOT_VALUE = 'bar';

let SnapshotFile;

describe('SnapshotFile', () => {
beforeEach(() => {
accessShouldThrow = false;
SnapshotFile = require('../SnapshotFile');
});

it('can tell if a snapshot file exists or not', () => {
const snapshotFile = SnapshotFile.forFile(TEST_FILE);
accessShouldThrow = false;
expect(snapshotFile.fileExists()).toBe(true);
accessShouldThrow = true;
expect(snapshotFile.fileExists()).toBe(false);
});

it('stores and retrieves snapshots', () => {
const snapshotFile = SnapshotFile.forFile(TEST_FILE);
snapshotFile.add(SNAPSHOT, SNAPSHOT_VALUE);
expect(snapshotFile.get(SNAPSHOT)).toBe(SNAPSHOT_VALUE);
});

it('can tell if a snapshot file has a snapshot', () => {
const NOT_A_SNAPSHOT = 'baz';
const snapshotFile = SnapshotFile.forFile(TEST_FILE);
snapshotFile.add(SNAPSHOT, SNAPSHOT_VALUE);
expect(snapshotFile.has(SNAPSHOT)).toBe(true);
expect(snapshotFile.has(NOT_A_SNAPSHOT)).toBe(false);
});

it('can tell if a snapshot matches a string', () => {
const INCORRECT_VALUE = 'baz';
const snapshotFile = SnapshotFile.forFile(TEST_FILE);
snapshotFile.add(SNAPSHOT, SNAPSHOT_VALUE);
expect(snapshotFile.matches(SNAPSHOT, SNAPSHOT_VALUE)).toBe(true);
expect(snapshotFile.matches(SNAPSHOT, INCORRECT_VALUE)).toBe(false);
});

it('can replace snapshot values', () => {
const NEW_VALUE = 'baz';
const snapshotFile = SnapshotFile.forFile(TEST_FILE);
snapshotFile.add(SNAPSHOT, SNAPSHOT_VALUE);
expect(snapshotFile.matches(SNAPSHOT, SNAPSHOT_VALUE)).toBe(true);
snapshotFile.add(SNAPSHOT, NEW_VALUE);
expect(snapshotFile.matches(SNAPSHOT, NEW_VALUE)).toBe(true);
});

it('can add the same key twice', () => {
const snapshotFile = SnapshotFile.forFile(TEST_FILE);
snapshotFile.add(SNAPSHOT, SNAPSHOT_VALUE);
expect(
() => snapshotFile.add(SNAPSHOT, SNAPSHOT_VALUE)
).not.toThrow();
});

it('loads and saves file correctly', () => {
const snapshotFile = SnapshotFile.forFile(TEST_FILE);
snapshotFile.add(SNAPSHOT, SNAPSHOT_VALUE);
expect(snapshotFile.get(SNAPSHOT)).toBe(SNAPSHOT_VALUE);
snapshotFile.save();
});
});
Loading

0 comments on commit dc7f3a9

Please sign in to comment.