This document covers how to run and write tests for Countly Server.
Tests are run using Grunt task runner with:
- Mocha.js — Test framework
- should.js — Assertion library
- supertest — HTTP request testing
Run the complete test suite (requires clean Countly instance without admin user or apps):
countly testThis runs ESLint validation followed by test suites in order:
- frontend — Setup, login tests
- api — User, app, token management tests
- api.write — Write operations, bulk requests
- plugins — All enabled plugin tests
- cleanup — Delete test data, close connections
- unit-tests — Unit tests for utility functions
# Unit tests only (no Docker required)
npm run test:unit
# Core API tests
npm run test:api-core
# CE plugin tests
npm run test:lite-plugins
# EE plugin tests
npm run test:enterprise-plugins
# Single plugin tests
npm run test:plugin -- <pluginname># Run tests for a specific plugin
countly plugin test pluginname
# Run tests for multiple plugins
countly plugin test plugin1 plugin2 plugin3
# Run only the test file (skip app/user creation)
countly plugin test pluginname --only
# Debug mode (pause between test cases)
countly plugin test pluginname --debug
# Combine options
countly plugin test plugin1 plugin2 --only --debugDebug mode pauses after each test case, allowing you to:
- Check the dashboard between tests
- Inspect database state
- Press Enter to continue to next test
Place tests in one of these locations:
plugins/<name>/tests.js— Single test fileplugins/<name>/tests/index.js— Multiple test files
Tests should follow this pattern:
- Empty state — Verify correct behavior with no data
- Write data — Use SDK or API to create data
- Verify data — Confirm data was stored correctly
- Cleanup — Reset app and verify cleanup
var request = require('supertest');
var should = require('should');
var testUtils = require("../../test/testUtils");
request = request(testUtils.url);
var APP_KEY = "";
var API_KEY_ADMIN = "";
var APP_ID = "";
var DEVICE_ID = "1234567890";
describe('Testing My Plugin', function() {
// 1. Test empty state
describe('Empty state', function() {
it('should have no data', function(done) {
// Get test credentials
API_KEY_ADMIN = testUtils.get("API_KEY_ADMIN");
APP_ID = testUtils.get("APP_ID");
APP_KEY = testUtils.get("APP_KEY");
request
.get('/o?api_key=' + API_KEY_ADMIN + '&app_id=' + APP_ID + '&method=myplugin')
.expect(200)
.end(function(err, res) {
if (err) return done(err);
var ob = JSON.parse(res.text);
ob.should.be.empty;
setTimeout(done, 100);
});
});
});
// 2. Write data
describe('Writing data', function() {
it('should succeed', function(done) {
var params = {"my_metric": "value1"};
request
.get('/i?device_id=' + DEVICE_ID + '&app_key=' + APP_KEY + '&begin_session=1&metrics=' + JSON.stringify(params))
.expect(200)
.end(function(err, res) {
if (err) return done(err);
var ob = JSON.parse(res.text);
ob.should.have.property('result', 'Success');
setTimeout(done, 100);
});
});
});
// 3. Verify data
describe('Verify data', function() {
it('should have written data', function(done) {
request
.get('/o?api_key=' + API_KEY_ADMIN + '&app_id=' + APP_ID + '&method=myplugin')
.expect(200)
.end(function(err, res) {
if (err) return done(err);
var ob = JSON.parse(res.text);
ob.should.have.property('my_metric');
setTimeout(done, 100);
});
});
});
// 4. Cleanup
describe('Reset app', function() {
it('should reset data', function(done) {
var params = {app_id: APP_ID};
request
.get('/i/apps/reset?api_key=' + API_KEY_ADMIN + '&args=' + JSON.stringify(params))
.expect(200)
.end(function(err, res) {
if (err) return done(err);
var ob = JSON.parse(res.text);
ob.should.have.property('result', 'Success');
setTimeout(done, 100);
});
});
});
// 5. Verify cleanup
describe('Verify cleanup', function() {
it('should have no data after reset', function(done) {
request
.get('/o?api_key=' + API_KEY_ADMIN + '&app_id=' + APP_ID + '&method=myplugin')
.expect(200)
.end(function(err, res) {
if (err) return done(err);
var ob = JSON.parse(res.text);
ob.should.be.empty;
setTimeout(done, 100);
});
});
});
});var testUtils = require("../../test/testUtils");
// Get test credentials
testUtils.get("API_KEY_ADMIN"); // Admin API key
testUtils.get("APP_ID"); // Test app ID
testUtils.get("APP_KEY"); // Test app key
// Base URL for requests
testUtils.url;// Validate metric responses
testUtils.validateMetrics(err, res, done, {
meta: {"browser": ['Chrome']},
"Chrome": {"n": 1, "t": 1, "u": 1}
});The full test suite runs in this order:
| Suite | Tests |
|---|---|
| frontend | DB connection, setup page, login |
| api | Config, empty API, user CRUD, apps, tokens |
| api.write | Session writes, metrics, events, bulk, checksums |
| plugins | All enabled plugin tests |
| cleanup | Delete apps/users, close DB |
| unit-tests | Common utility unit tests |
- Start clean, end clean — Tests should leave the app in the same state they found it
- Test edge cases — Include invalid inputs, missing parameters, unauthorized access
- Use timeouts — Add
setTimeout(done, 100)to allow async operations to complete - Isolate tests — Each test should be independent and not rely on side effects
- Test lifecycle handlers — Verify
/i/apps/create,/i/apps/delete,/i/apps/resethandlers
All code must pass ESLint before tests run:
# Lint specific plugin
countly plugin lint <pluginname>
countly plugin lintfix <pluginname>
# Lint shell scripts
countly shellcheckTests run automatically on pull requests via GitHub Actions. Ensure:
- All tests pass locally before pushing
- No ESLint errors
- Shell scripts pass shellcheck validation