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" + ] +}