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 d170720..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": "echo \"Error: no test specified\" && exit 1", + "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" @@ -26,16 +28,30 @@ }, "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-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", "tslint": "^5.9.1", "tslint-config-prettier": "^1.14.0", "tslint-plugin-prettier": "^1.3.0", 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-unit-config.js b/rollup.test-unit-config.js new file mode 100644 index 0000000..fc8c095 --- /dev/null +++ b/rollup.test-unit-config.js @@ -0,0 +1,32 @@ +"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'; + +export default { + input: 'test/unit/**/*.test.ts', + output: { + file: 'dist/test-unit.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.json"}), + multiEntry(), + buble() + ] +} 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/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')); diff --git a/test/unit/main.test.ts b/test/unit/main.test.ts new file mode 100644 index 0000000..614c830 --- /dev/null +++ b/test/unit/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()); + }); +}); diff --git a/test/unit/mock.ts b/test/unit/mock.ts new file mode 100644 index 0000000..a7a868f --- /dev/null +++ b/test/unit/mock.ts @@ -0,0 +1,10 @@ +export const Game = { + creeps: [], + rooms: [], + spawns: {}, + time: 12345 +}; + +export const Memory = { + creeps: [] +}; 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" + ] +}