From cba7463fd1e5a22a5306370cf8db4e0756c76cb5 Mon Sep 17 00:00:00 2001 From: Austin Rivas Date: Wed, 10 Oct 2018 02:17:40 -0400 Subject: [PATCH 1/4] added mocha/chai/sinon test config --- package.json | 14 +++++++++++++- rollup.test-config.js | 30 ++++++++++++++++++++++++++++++ test/mocha.opts | 12 ++++++++++++ test/setup-node.js | 6 ++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 rollup.test-config.js create mode 100644 test/mocha.opts create mode 100644 test/setup-node.js diff --git a/package.json b/package.json index d170720..aedefc3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "push-main": "rollup -c --environment DEST:main", "push-pserver": "rollup -c --environment DEST:pserver", "push-sim": "rollup -c --environment DEST:sim", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "rollup -c rollup.test-config.js && mocha dist/test.bundle.js", "watch-main": "rollup -cw --environment DEST:main", "watch-pserver": "rollup -cw --environment DEST:pserver", "watch-sim": "rollup -cw --environment DEST:sim" @@ -26,16 +26,28 @@ }, "homepage": "https://github.com/screepers/screeps-typescript-starter#readme", "devDependencies": { + "@types/chai": "^4.1.6", "@types/lodash": "^3.10.1", + "@types/mocha": "^5.2.5", "@types/node": "^10.5.5", "@types/screeps": "^2.4.0", + "@types/sinon": "^5.0.5", + "@types/sinon-chai": "^3.2.0", + "chai": "^4.2.0", + "lodash": "^4.17.11", + "mocha": "^5.2.0", "prettier": "^1.14.0", "rollup": "^0.63.4", + "rollup-plugin-buble": "^0.19.4", "rollup-plugin-clear": "^2.0.7", "rollup-plugin-commonjs": "^9.1.4", + "rollup-plugin-multi-entry": "^2.0.2", "rollup-plugin-node-resolve": "^3.3.0", "rollup-plugin-screeps": "^0.1.2", "rollup-plugin-typescript2": "^0.16.1", + "sinon": "^6.3.5", + "sinon-chai": "^3.2.0", + "ts-node": "^7.0.1", "tslint": "^5.9.1", "tslint-config-prettier": "^1.14.0", "tslint-plugin-prettier": "^1.3.0", diff --git a/rollup.test-config.js b/rollup.test-config.js new file mode 100644 index 0000000..eb7470b --- /dev/null +++ b/rollup.test-config.js @@ -0,0 +1,30 @@ +"use strict"; + +import resolve from "rollup-plugin-node-resolve"; +import commonjs from "rollup-plugin-commonjs"; +import typescript from "rollup-plugin-typescript2"; +import buble from 'rollup-plugin-buble'; +import multiEntry from 'rollup-plugin-multi-entry'; + +export default { + input: 'test/**/*.test.ts', + output: { + file: 'dist/test.bundle.js', + name: 'lib', + sourcemap: true, + format: 'iife', + globals: { + chai: 'chai', + it: 'it', + describe: 'describe' + } + }, + external: ['chai', 'it', 'describe'], + plugins: [ + resolve(), + commonjs(), + typescript({tsconfig: "./tsconfig.json"}), + multiEntry(), + buble() + ] +} diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..e94ac68 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,12 @@ +--require test/setup-node.js +--require ts-node/register +--ui bdd + +--reporter spec +--bail +--full-trace +--watch-extensions tsx,ts +--colors + +--recursive +--timeout 5000 diff --git a/test/setup-node.js b/test/setup-node.js new file mode 100644 index 0000000..1975df8 --- /dev/null +++ b/test/setup-node.js @@ -0,0 +1,6 @@ +//inject mocha globally to allow custom interface refer without direct import - bypass bundle issue +global._ = require('lodash'); +global.mocha = require('mocha'); +global.chai = require('chai'); +global.sinon = require('sinon'); +global.chai.use(require('sinon-chai')); From 6a047c3d85a0680f31a16caff844d0e92a975c09 Mon Sep 17 00:00:00 2001 From: Austin Rivas Date: Wed, 10 Oct 2018 02:18:40 -0400 Subject: [PATCH 2/4] added mock game objects --- test/mock.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 test/mock.ts diff --git a/test/mock.ts b/test/mock.ts new file mode 100644 index 0000000..a7a868f --- /dev/null +++ b/test/mock.ts @@ -0,0 +1,10 @@ +export const Game = { + creeps: [], + rooms: [], + spawns: {}, + time: 12345 +}; + +export const Memory = { + creeps: [] +}; From 1bf84c24430bce4b4b8cab3692fb8b9353a20af3 Mon Sep 17 00:00:00 2001 From: Austin Rivas Date: Wed, 10 Oct 2018 02:20:10 -0400 Subject: [PATCH 3/4] added tests for main game loop --- test/main.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/main.test.ts diff --git a/test/main.test.ts b/test/main.test.ts new file mode 100644 index 0000000..caa7095 --- /dev/null +++ b/test/main.test.ts @@ -0,0 +1,25 @@ +import {assert} from "chai"; +import {loop} from "../src/main"; +import {Game, Memory} from "./mock" + +describe("main", () => { + before(() => { + // runs before all test in this block + }); + + beforeEach(() => { + // runs before each test in this block + // @ts-ignore : allow adding Game to global + global.Game = _.clone(Game); + // @ts-ignore : allow adding Memory to global + global.Memory = _.clone(Memory); + }); + + it("should export a loop function", () => { + assert.isTrue(typeof loop === "function"); + }); + + it("should return void when called with no context", () => { + assert.isUndefined(loop()); + }); +}); From f8507b632a52abb59c3d8d983ba307cbb9efa7af Mon Sep 17 00:00:00 2001 From: kaen Date: Sat, 10 Nov 2018 15:49:43 -0800 Subject: [PATCH 4/4] add integration testing support --- .gitignore | 3 + docs/in-depth/testing.md | 71 +++++++++++++++++++ package.json | 6 +- rollup.test-integration-config.js | 34 +++++++++ ...st-config.js => rollup.test-unit-config.js | 6 +- test/integration/helper.ts | 63 ++++++++++++++++ test/integration/integration.test.ts | 18 +++++ test/{ => unit}/main.test.ts | 2 +- test/{ => unit}/mock.ts | 0 tsconfig.test-integration.json | 20 ++++++ 10 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 docs/in-depth/testing.md create mode 100644 rollup.test-integration-config.js rename rollup.test-config.js => rollup.test-unit-config.js (78%) create mode 100644 test/integration/helper.ts create mode 100644 test/integration/integration.test.ts rename test/{ => unit}/main.test.ts (94%) rename test/{ => unit}/mock.ts (100%) create mode 100644 tsconfig.test-integration.json diff --git a/.gitignore b/.gitignore index e23369d..c3d567f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ # Screeps Config screeps.json +# ScreepsServer data from integration tests +/server + # Numerous always-ignore extensions *.diff *.err diff --git a/docs/in-depth/testing.md b/docs/in-depth/testing.md new file mode 100644 index 0000000..37582b9 --- /dev/null +++ b/docs/in-depth/testing.md @@ -0,0 +1,71 @@ +# Testing + +Automated testing helps prevent regressions and reproduce complex failure +scenarios for bug fixing or feature implementation. This project comes with +support for both unit and integration testing with your Screeps code. + +You can read more about [unit and integration testing on +Wikipedia](https://en.wikipedia.org/wiki/Test-driven_development). + +This documentation will cover the testing setup for those already familiar with +the process of test driven design. + +Tests are written via [Mocha](https://mochajs.org/) and executed as tests only +if they include `.test.ts` in their filename. If you have written a test file +but aren't seeing it executed, this is probably why. There are two separate test +commands and configurations, as unit tests don't need the complete Screeps +server run-time as integration tests do. + +## Running Tests + +The standard `npm test` will execute all unit and integration tests in sequence. +This is helpful for CI/CD and pre-publish checks, however during active +development it's better to run just a subset of interesting tests. + +You can use `npm run test-unit` or `npm run test-integration` to run just one of +the test suites. Additionally you can supply Mocha options to these test +commands to further control the testing behavior. As an example, the following +command will only execute integration tests with the word `memory` in their +description: + +``` +npm run test-integration -- -g memory +``` + +Note that arguments after the initial `--` will be passed to `mocha` directly. + +## Unit Testing + +You can test code with simple run-time dependencies via the unit testing +support. Since unit testing is much faster than integration testing by orders of +magnitude, it is recommended to prefer unit tests wherever possible. + +## Integration Testing + +Integration testing is for code that depends heavily on having a full game +environment. Integration tests are completely representative of the real game +(in fact they run with an actual Screeps server). This comes at the cost of +performance and very involved setup when creating specific scenarios. + +Server testing support is implmented via +[screeps-server-mockup](https://github.com/Hiryus/screeps-server-mockup). View +this repository for more information on the API. + +By default the test helper will create a "stub" world with a 3x3 grid of rooms +with sources and controllers. Additionally it spawns a bot called "player" +running the compiled main.js file from this repository. + +It falls on the user to properly set up preconditions using the +screeps-server-mockup API. Importantly, most methods exposed with this API are +asynchronous, so using them requires frequent use of the `await` keyword to get +a result and ensure order of execution. If you find that some of your +preconditions don't seem to take effect, or that you receive a Promise object +rather than an expected value, you're likely missing `await` on an API method. + +Finally, please note that screeps-server-mockup, and this repo by extension, +come with a specific screeps server version at any given time. It's possible +that either your local package.json, or the screeps-server-mockup package itself +are out of date and pulling in an older version of the [screeps +server](https://github.com/screeps/screeps). If you notice that test environment +behavior differs from the MMO server, ensure that all of these dependencies are +correctly up to date. diff --git a/package.json b/package.json index aedefc3..9267b89 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "push-main": "rollup -c --environment DEST:main", "push-pserver": "rollup -c --environment DEST:pserver", "push-sim": "rollup -c --environment DEST:sim", - "test": "rollup -c rollup.test-config.js && mocha dist/test.bundle.js", + "test": "npm run test-unit && npm run test-integration", + "test-unit": "rollup -c rollup.test-unit-config.js && mocha dist/test-unit.bundle.js", + "test-integration": "npm run build && rollup -c rollup.test-integration-config.js && mocha dist/test-integration.bundle.js", "watch-main": "rollup -cw --environment DEST:main", "watch-pserver": "rollup -cw --environment DEST:pserver", "watch-sim": "rollup -cw --environment DEST:sim" @@ -43,8 +45,10 @@ "rollup-plugin-commonjs": "^9.1.4", "rollup-plugin-multi-entry": "^2.0.2", "rollup-plugin-node-resolve": "^3.3.0", + "rollup-plugin-nodent": "^0.2.2", "rollup-plugin-screeps": "^0.1.2", "rollup-plugin-typescript2": "^0.16.1", + "screeps-server-mockup": "^1.4.3", "sinon": "^6.3.5", "sinon-chai": "^3.2.0", "ts-node": "^7.0.1", diff --git a/rollup.test-integration-config.js b/rollup.test-integration-config.js new file mode 100644 index 0000000..556bdf7 --- /dev/null +++ b/rollup.test-integration-config.js @@ -0,0 +1,34 @@ +"use strict"; + +import clear from "rollup-plugin-clear"; +import resolve from "rollup-plugin-node-resolve"; +import commonjs from "rollup-plugin-commonjs"; +import typescript from "rollup-plugin-typescript2"; +import buble from 'rollup-plugin-buble'; +import multiEntry from 'rollup-plugin-multi-entry'; +import nodent from 'rollup-plugin-nodent'; + +export default { + input: 'test/integration/**/*.test.ts', + output: { + file: 'dist/test-integration.bundle.js', + name: 'lib', + sourcemap: true, + format: 'iife', + globals: { + chai: 'chai', + it: 'it', + describe: 'describe' + } + }, + external: ['chai', 'it', 'describe'], + plugins: [ + clear({ targets: ["dist/test.bundle.js"] }), + resolve(), + commonjs(), + typescript({tsconfig: "./tsconfig.test-integration.json"}), + nodent(), + multiEntry(), + buble() + ] +} diff --git a/rollup.test-config.js b/rollup.test-unit-config.js similarity index 78% rename from rollup.test-config.js rename to rollup.test-unit-config.js index eb7470b..fc8c095 100644 --- a/rollup.test-config.js +++ b/rollup.test-unit-config.js @@ -1,5 +1,6 @@ "use strict"; +import clear from "rollup-plugin-clear"; import resolve from "rollup-plugin-node-resolve"; import commonjs from "rollup-plugin-commonjs"; import typescript from "rollup-plugin-typescript2"; @@ -7,9 +8,9 @@ import buble from 'rollup-plugin-buble'; import multiEntry from 'rollup-plugin-multi-entry'; export default { - input: 'test/**/*.test.ts', + input: 'test/unit/**/*.test.ts', output: { - file: 'dist/test.bundle.js', + file: 'dist/test-unit.bundle.js', name: 'lib', sourcemap: true, format: 'iife', @@ -21,6 +22,7 @@ export default { }, external: ['chai', 'it', 'describe'], plugins: [ + clear({ targets: ["dist/test.bundle.js"] }), resolve(), commonjs(), typescript({tsconfig: "./tsconfig.json"}), diff --git a/test/integration/helper.ts b/test/integration/helper.ts new file mode 100644 index 0000000..b4dc5e7 --- /dev/null +++ b/test/integration/helper.ts @@ -0,0 +1,63 @@ +const { readFileSync } = require('fs'); +const _ = require('lodash'); +const { ScreepsServer, stdHooks } = require('screeps-server-mockup'); +const DIST_MAIN_JS = 'dist/main.js'; + +/* + * Helper class for creating a ScreepsServer and resetting it between tests. + * See https://github.com/Hiryus/screeps-server-mockup for instructions on + * manipulating the terrain and game state. + */ +class IntegrationTestHelper { + private _server; + private _player; + + get server() { + return this._server; + } + + get player() { + return this._player; + } + + async beforeEach() { + this._server = new ScreepsServer(); + + // reset world but add invaders and source keepers bots + await this._server.world.reset(); + + // create a stub world composed of 9 rooms with sources and controller + await this._server.world.stubWorld(); + + // add a player with the built dist/main.js file + const modules = { + main: readFileSync(DIST_MAIN_JS).toString(), + }; + this._player = await this._server.world.addBot({ username: 'player', room: 'W0N1', x: 15, y: 15, modules }); + + // Start server + await this._server.start(); + } + + async afterEach() { + await this._server.stop(); + } +} + +beforeEach(async () => { + await helper.beforeEach(); +}); + +afterEach(async () => { + await helper.afterEach(); +}); + +before(() => { + stdHooks.hookWrite(); +}); + +after(() => { + process.exit(); +}) + +export const helper = new IntegrationTestHelper(); diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts new file mode 100644 index 0000000..28a8676 --- /dev/null +++ b/test/integration/integration.test.ts @@ -0,0 +1,18 @@ +import {assert} from "chai"; +import {helper} from "./helper"; + +describe("main", () => { + it("runs a server and matches the game tick", async function () { + for (let i = 1; i < 10; i += 1) { + assert.equal(await helper.server.world.gameTime, i); + await helper.server.tick(); + } + }); + + it("writes and reads to memory", async function () { + await helper.player.console(`Memory.foo = 'bar'`); + await helper.server.tick(); + const memory = JSON.parse(await helper.player.memory); + assert.equal(memory.foo, 'bar'); + }); +}); diff --git a/test/main.test.ts b/test/unit/main.test.ts similarity index 94% rename from test/main.test.ts rename to test/unit/main.test.ts index caa7095..614c830 100644 --- a/test/main.test.ts +++ b/test/unit/main.test.ts @@ -1,5 +1,5 @@ import {assert} from "chai"; -import {loop} from "../src/main"; +import {loop} from "../../src/main"; import {Game, Memory} from "./mock" describe("main", () => { diff --git a/test/mock.ts b/test/unit/mock.ts similarity index 100% rename from test/mock.ts rename to test/unit/mock.ts diff --git a/tsconfig.test-integration.json b/tsconfig.test-integration.json new file mode 100644 index 0000000..875a94b --- /dev/null +++ b/tsconfig.test-integration.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "esnext", + "lib": ["esnext"], + "target": "es5", + "moduleResolution": "Node", + "outDir": "dist", + "baseUrl": "src/", + "sourceMap": true, + "strict": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "noImplicitAny": false, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false + }, + "exclude": [ + "node_modules" + ] +}