What is the best way to test a HapiJS plugin, for example one plugin that add routes and handlers.
Since I have to create an instance of Hapi.Server to run the plugins, should I define all the tests from the app's root, for all the plugins ?
or
should I manage to get THE instance of Hapi.Server in my plugin's local tests ?
If I go for the second option, my server will have registered all the plugins, including those that the plugin to be tested doesn't depends on.
What is the best way to approach this ?
Thanks in advance.
If you're using Glue (and I highly recommend it), you can create a manifest variable for each test (or group of tests) you want to execute. The manifest only needs to include plugins required for that test to execute properly.
And expose some sort of init function to actually start your server. Small example:
import Lab = require("lab");
import Code = require('code');
import Path = require('path');
import Server = require('../path/to/init/server');
export const lab = Lab.script();
const it = lab.it;
const describe = lab.describe;
const config = {...};
const internals = {
manifest: {
connections: [
{
host: 'localhost',
port: 0
}
],
registrations: [
{
plugin: {
register: '../http_routes',
options: config
}
},
{
plugin: {
register: '../business_plugin',
options: config
}
}
]
},
composeOptions: {
relativeTo: 'some_path'
}
};
describe('business plugin', function () {
it('should do some business', function (done) {
Server.init(internals.manifest, internals.composeOptions, function (err, server) {
// run your tests here
});
});
});
init function:
export const init = function (manifest: any, composeOptions: any, next: (err?: any, server?: Hapi.Server) => void) {
Glue.compose(manifest, composeOptions, function (err: any, server: Hapi.Server) {
if (err) {
return next(err);
}
server.start(function (err: any) {
return next(err, server);
});
});
};
Related
I am writing an application to handle requests and return predefined responses to allow testing of external REST endpoints by software that cannot have internal tests written into it currently. Therefore, my code uses the Nest JS framework to handle the routes, and then extracts values and returns data. The data returned is stored in external files.
To handle constant changes and different team usage, the program uses a .env file to give the base (root) directory where the files to respond are located. I am trying to write a test case to ensure that the NestJS ConfigService is working properly, but also to use as a base for all my other tests.
With different routes, different data files need to be returned. My code will need to mock all these files. As this data relies on the base ConfigService having read the .env to find the base paths, my routes are based on this starting point.
During development, I have a local .env file with these values set. However, I want to test without this .env file being used, so my tests do not rely on the presence of the .env file, since the CI/CD server, build server, etc., will not have a .env file.
I am simply trying a test file then to work with the configuration, to get set data from my mock.
nestjs-config.service.spec.ts
import { Test, TestingModule } from '#nestjs/testing';
import { ConfigModule, ConfigService } from '#nestjs/config';
describe('NestJS Configuration .env', () => {
let service: ConfigService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
expandVariables: true,
}),
],
providers: [
{
provide: ConfigService,
useValue: {
get: jest.fn((key: string) => {
if (key === 'FILES') {
return './fakedata/';
} else if (key === 'PORT') {
return '9999';
}
return null;
}),
},
},
],
}).compile();
service = module.get<ConfigService>(ConfigService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it.each([
['FILES=', 'FILES', './', './fakedata/'],
['PORT=', 'PORT', '2000', '9999'],
['default value when key is not found', 'NOTFOUND', './', './'],
])('should get from the .env file, %s', (Text: string, Key: string, Default: string, Expected: string) => {
const Result: string = service.get<string>(Key, Default);
expect(Key).toBeDefined();
expect(Result).toBe(Expected);
});
});
The problem in this test is the default values are always returned, meaning the .env file was not read, but the provider had the code to handle this.
Ideally, I would like to create a fake class for testing so I could use it in all my test files. However, when trying to create the fake class, I get an error about other methods missing, that are unrelated to this class.
export class ConfigServiceFake {
get(key: string) {
switch (key) {
case 'FILES':
return './fakedata/';
case 'PORT':
return '9999';
}
}
}
This does not seem to execute, and it appears to still go through the original service.
I was able to adjust this and not need external references, including importing the configuration module, making the mock simpler, and not needing the full definitions.
import { Test, TestingModule } from '#nestjs/testing';
import { ConfigService } from '#nestjs/config';
describe('NestJS Configuration .env', () => {
let service: ConfigService;
afterEach(() => {
jest.clearAllMocks();
});
beforeEach(async () => {
const FakeConfigService = {
provide: ConfigService,
useValue: {
get: jest.fn((Key: string, DefaultValue: string) => {
switch (Key) {
case 'FILES':
return './fakedata/';
break;
case 'PORT':
return '9999';
break;
default:
return DefaultValue;
}
}),
},
};
const module: TestingModule = await Test.createTestingModule({
providers: [FakeConfigService],
}).compile();
service = module.get<ConfigService>(ConfigService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it.each([
['FILES=', 'FILES', './', './fakedata/'],
['PORT=', 'PORT', '2000', '9999'],
['default value when key is not found', 'NOTFOUND', './', './'],
])('should get from the .env file, %s', (Text: string, Key: string, Default: string, Expected: string) => {
const Result: string = service.get<string>(Key, Default);
expect(Key).toBeDefined();
expect(Result).toBe(Expected);
});
});
I am working on a StencilJS project where I have to use MirageJS to make fake API data.
How to call server before StencilJS application loads.
In react we can call makeServer() in the index.ts file, but in the stencil, we don't have such a file.
How can we call this to start the mirage server, Please can someone suggest the correct way.
Below is my server.ts file
mirage/server.ts
import { createServer, Model } from 'miragejs';
import { auditFactory } from './factories';
import { processCollectionRequest } from './utils';
export const makeServer = async ({ environment = 'development' } = {}) => {
console.log('started server');
return createServer({
environment,
factories: {
people: auditFactory,
},
models: {
people: Model,
},
routes() {
this.namespace = '/api/v1';
this.get('/peoples', function (schema, request) {
let res = processCollectionRequest(schema, request, 'peoples', this);
// remove factory properties not in spec
res.items.forEach(e => ['associatedResourceId', 'associatedResourceName', 'associatedResourceType'].forEach(p => delete e[p]));
return res;
});
},
seeds(server) {
server.createList('audit', 20);
},
});
};
I'm not familiar with MirageJS so I might be off, but can you use globalScript (https://stenciljs.com/docs/config) and then run your Mirage server there?
I have the following rollup plugin that will include .feature files
export const cucumberRollupPlugin: PluginImpl<CucumberOptions> = pluginOptions => {
let options: CucumberOptions = {
...{
include: '**/*.feature',
cwd: process.cwd(),
},
...pluginOptions,
};
let filter = createFilter(options);
let plugin: Plugin = {
name: 'bigtest-cucumber',
async transform(code, id) {
if (!filter(id)) {
return;
}
let parser = new GherkinParser({ code, uri: id, rootDir: options.cwd });
let result = await parser.parse();
let esm = dataToEsm(result, { namedExports: false });
// TODO: add sourcemap support
let transformResult: TransformResult = { code: esm };
return transformResult;
},
};
return plugin;
};
The problem I have is that to make the feature files work, there are step definition files that actually contain the functionality. So a feature file might look like this
Feature: Visit career guide page in career.guru99.com
Scenario: Visit career.guru99.com
Given: I browse to career.guru99.com
And a step definition file might look like this:
import { Given, When, Then from 'cucumber';
import assert from 'assert';
import{ driver } from '../support/web_driver';
Given(/^browse to web site "([^"]*)"$/, async function(url) {
return driver.get(url);
});
The problem I have with rollup is that there are no import statements for either the step definition files. The way cucumber-js works is that these files are found at runtime.
I think I need to generate an index.js that looks like this to cover the step definitions.
import from './step_definition_1';
import from './step_definition_2';
import from './step_definition_3';
Where would this fit in the rollup pipeline to generate this file so it can get pulled into the rollup pipeline.
You can use this.emitFile to manually process the .feature files and include them in the output. Call this.emitFile for each .feature file in the buildStart hook (each emitted file will get processed through the transform hook you wrote).
Here's an example that uses the globby package (which expands a glob to an array of file paths) to get the file path of each .feature file to pass to this.emitFile:
import globby from 'globby'
export const cucumberRollupPlugin: PluginImpl<CucumberOptions> = pluginOptions => {
let options: CucumberOptions = {
include: '**/*.feature',
cwd: process.cwd(),
...pluginOptions,
};
let filter = createFilter(options);
let plugin: Plugin = {
name: 'bigtest-cucumber',
async buildStart({ include, cwd }) {
const featureFilePaths = await globby(include, { cwd });
for (const featureFilePath of featureFilePaths) {
this.emitFile({
type: 'chunk',
id: featureFilePath
});
}
},
async transform(code, id) {
if (!filter(id)) {
return;
}
let parser = new GherkinParser({ code, uri: id, rootDir: options.cwd });
let result = await parser.parse();
let esm = dataToEsm(result, { namedExports: false });
// TODO: add sourcemap support
let transformResult: TransformResult = { code: esm };
return transformResult;
},
};
return plugin;
};
Let me know if you have any questions!
You need to use the this.emitFile method and befor that lookup the files via glob or anything else inside your plugin
{
plugins: [typescript(),{
name: "emit-additional-files",
async transform(code,id) {
//id === fileName
//code === fileContent
// inspect code or id here then use path.resolve() and fs.readdir to find additional files
this.emitFile()
}
}]
}
I try to test my web services, hosted in my Next.js app and I have an error with not found Next.js configuration.
My web service are regular one, stored in the pages/api directory.
My API test fetches a constant ATTACKS_ENDPOINT thanks to this file:
/pages/api/tests/api.spec.js
import { ATTACKS_ENDPOINT } from "../config"
...
describe("endpoints", () => {
beforeAll(buildOptionsFetch)
it("should return all attacks for attacks endpoint", async () => {
const response = await fetch(API_URL + ATTACKS_ENDPOINT, headers)
config.js
import getConfig from "next/config"
const { publicRuntimeConfig } = getConfig()
export const API_URL = publicRuntimeConfig.API_URL
My next.config.js is present and is used properly by the app when started.
When the test is run, this error is thrown
TypeError: Cannot destructure property `publicRuntimeConfig` of 'undefined' or 'null'.
1 | import getConfig from "next/config"
2 |
> 3 | const { publicRuntimeConfig } = getConfig()
I looked for solutions and I found this issue which talks about _manually initialise__ next app.
How to do that, given that I don't test React component but API web service ?
I solved this problem by creating a jest.setup.js file and adding this line of code
First add jest.setup.js to jest.config.js file
// jest.config.js
module.exports = {
// Your config
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
AND then
// jest.setup.js
jest.mock('next/config', () => () => ({
publicRuntimeConfig: {
YOUR_PUBLIC_VARIABLE: 'value-of-env' // Change this line and copy your env
}
}))
OR
// jest.setup.js
import { setConfig } from 'next/config'
import config from './next.config'
// Make sure you can use "publicRuntimeConfig" within tests.
setConfig(config)
The problem I faced with testing with Jest was that next was not being initialized as expected. My solution was to mock the next module... You can try this:
/** #jest-environment node */
jest.mock('next');
import next from 'next';
next.mockReturnValue({
prepare: () => Promise.resolve(),
getRequestHandler: () => (req, res) => res.status(200),
getConfig: () => ({
publicRuntimeConfig: {} /* This is where you import the mock values */
})
});
Read about manual mocks here: https://jestjs.io/docs/en/manual-mocks
In my case, I had to:
Create a jest.setup.js file and
setConfig({
...config,
publicRuntimeConfig: {
BASE_PATH: '/',
SOME_KEY: 'your_value',
},
serverRuntimeConfig: {
YOUR_KEY: 'your_value',
},
});
Then add this in your jest.config.js file:
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
Given the example official Nuxt end-to-end test example using Ava:
import test from 'ava'
import { Nuxt, Builder } from 'nuxt'
import { resolve } from 'path'
// We keep a reference to Nuxt so we can close
// the server at the end of the test
let nuxt = null
// Init Nuxt.js and start listening on localhost:4000
test.before('Init Nuxt.js', async t => {
const rootDir = resolve(__dirname, '..')
let config = {}
try { config = require(resolve(rootDir, 'nuxt.config.js')) } catch (e) {}
config.rootDir = rootDir // project folder
config.dev = false // production build
config.mode = 'universal' // Isomorphic application
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(4000, 'localhost')
})
// Example of testing only generated html
test('Route / exits and render HTML', async t => {
let context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('<h1 class="red">Hello world!</h1>'))
})
// Close the Nuxt server
test.after('Closing server', t => {
nuxt.close()
})
How can you use Nuxt or Builder to configure/access the applications Vuex store? The example Vuex store would look like:
import Vuex from "vuex";
const createStore = () => {
return new Vuex.Store({
state: () => ({
todo: null
}),
mutations: {
receiveTodo(state, todo) {
state.todo = todo;
}
},
actions: {
async nuxtServerInit({ commit }, { app }) {
console.log(app);
const todo = await app.$axios.$get(
"https://jsonplaceholder.typicode.com/todos/1"
);
commit("receiveTodo", todo);
}
}
});
};
export default createStore;
Currently trying to run the provided Ava test, leads to an error attempting to access #nuxtjs/axios method $get:
TypeError {
message: 'Cannot read property \'$get\' of undefined',
}
I'd be able to mock $get and even $axios available on app in Vuex store method nuxtServerInit, I just need to understand how to access app in the test configuration.
Thank you for any help you can provide.
Just encountered this and after digging so many tutorial, I pieced together a solution.
You have essentially import your vuex store into Nuxt when using it programmatically. This is done by:
Importing Nuxt's config file
Adding to the config to turn off everything else but enable store
Load the Nuxt instance and continue your tests
Here's a working code (assuming your ava and dependencies are set up)
// For more info on why this works, check this aweomse guide by this post in getting this working
// https://medium.com/#brandonaaskov/how-to-test-nuxt-stores-with-jest-9a5d55d54b28
import test from 'ava'
import jsdom from 'jsdom'
import { Nuxt, Builder } from 'nuxt'
import nuxtConfig from '../nuxt.config' // your nuxt.config
// these boolean switches turn off the build for all but the store
const resetConfig = {
loading: false,
loadingIndicator: false,
fetch: {
client: false,
server: false
},
features: {
store: true,
layouts: false,
meta: false,
middleware: false,
transitions: false,
deprecations: false,
validate: false,
asyncData: false,
fetch: false,
clientOnline: false,
clientPrefetch: false,
clientUseUrl: false,
componentAliases: false,
componentClientOnly: false
},
build: {
indicator: false,
terser: false
}
}
// We keep a reference to Nuxt so we can close
// the server at the end of the test
let nuxt = null
// Init Nuxt.js and start listening on localhost:5000 BEFORE running your tests. We are combining our config file with our resetConfig using Object.assign into an empty object {}
test.before('Init Nuxt.js', async (t) => {
t.timeout(600000)
const config = Object.assign({}, nuxtConfig, resetConfig, {
srcDir: nuxtConfig.srcDir, // don't worry if its not in your nuxt.config file. it has a default
ignore: ['**/components/**/*', '**/layouts/**/*', '**/pages/**/*']
})
nuxt = new Nuxt(config)
await new Builder(nuxt).build()
nuxt.listen(5000, 'localhost')
})
// Then run our tests using the nuxt we defined initially
test.serial('Route / exists and renders correct HTML', async (t) => {
t.timeout(600000) // Sometimes nuxt's response is slow. We increase the timeont to give it time to render
const context = {}
const { html } = await nuxt.renderRoute('/', context)
t.true(html.includes('preload'))
// t.true(true)
})
test.serial('Route / exits and renders title', async (t) => {
t.timeout(600000)
const { html } = await nuxt.renderRoute('/', {})
const { JSDOM } = jsdom // this was the only way i could get JSDOM to work. normal import threw a functione error
const { document } = (new JSDOM(html)).window
t.true(document.title !== null && document.title !== undefined) // simple test to check if site has a title
})
Doing this should work. HOWEVER, You may still get some errors
✖ Timed out while running tests. If you get this you're mostly out of luck. I thought the problem was with Ava given that it didn't give a descriptive error (and removing any Nuxt method seemed to fix it), but so far even with the above snippet sometimes it works and sometimes it doesn't.
My best guess at this time is that there is a delay on Nuxt's side using either renderRouter or renderAndGetWindow that ava doesn't wait for, but on trying any of these methods ava almost immediately "times out" despite the t.timeout being explicitly set for each test. So far my research has lead me to checking the timeout for renderAndGetWindow (if it exists, but the docs doesn't indicate such).
That's all i've got.