nrwl / nx

Smart Monorepos · Fast CI
https://nx.dev
MIT License
23.7k stars 2.37k forks source link

Running nestjs e2e without having to serve app manually. devServerTarget for @nx/jest:jest task executor. #19367

Open PatrickMunsey opened 1 year ago

PatrickMunsey commented 1 year ago

Description

Running e2e tests on the nx generated nestjs appication fails unless you have manually served the associated application before hand.

Desired behaviour

nestjs/ jests e2e applications should be able to serve their associated applications without intervention. I've seen cypress e2e executors specify a devServerTarget config input in the project.json file. I'm wondering if a similar setup can be made for nestjs e2e tests.

npx create-nx-workspace --pm yarn --preset=nest --workspaceType=integrated --name=workspace --appName=api --ci=github --docker=false --nxCloud=false

cd workspace
yarn nx run api-e2e:e2e

Motivation

I am trying to run a command such as yarn nx run-many -t e2e in our ci without having to orchestrate serving at the same time, Automatically serving the application for the e2e with a single command will also make local testing easier. e2e tests for our fronted using playwright already work with the desired behaviour. e.g. yarn nx app-e2e:e2e succeeds non-interactively

Suggested Implementation

Example project.json file for specifying what application should be served prior to and during the e2e execution

{
  "name": "api-e2e",
  "$schema": "../../../node_modules/nx/schemas/project-schema.json",
  "projectType": "application",
  "targets": {
    "e2e": {
      "executor": "@nx/jest:jest",
      "outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"],
      "options": {
        "jestConfig": "apps/api-e2e/jest.config.ts",
        "passWithNoTests": true,
        "devServerTarget": "api:serve:development"
      }
    },
    "lint": {
      "executor": "@nx/linter:eslint",
      "outputs": ["{options.outputFile}"],
      "options": {
        "lintFilePatterns": ["apps/api-e2e/**/*.{js,ts}"]
      }
    }
  },
  "implicitDependencies": ["api"]
}

Alternate Implementations

benjaminhera commented 10 months ago

Any update on this? Or duplicate issue?

gperdomor commented 9 months ago

@AgentEnder @FrozenPandaz @vsavkin Hi guys, I think this is a very useful feature, can be included in the Nx 19 release? Thank you

SirPhemmiey commented 8 months ago

Any update on this, please?

codePassion-dot commented 8 months ago

Any updates ?

codePassion-dot commented 8 months ago

if this is not going to be implemented what is the recommended approach to serve the application after start services that the app needs (docker-compose, database, etc) ?

SirPhemmiey commented 8 months ago

@codePassion-dot the recommended approach would be to have a task/target that runs docker commands (which spins up your application) and run the e2e right after

john-james-gh commented 7 months ago

I believe it's already provided.

// jest.config.ts

export default {
  displayName: 'api-e2e',
  globalSetup: '<rootDir>/src/support/global-setup.ts',
  globalTeardown: '<rootDir>/src/support/global-teardown.ts',
  ...
};

// global-setup.ts

import { spawn } from 'child_process';

module.exports = async function () {
  // Start services that that the app needs to run (e.g. database, docker-compose, etc.).
  console.log('\nSetting up...\n');

  // Start the API server
  const server = spawn('nx', ['serve', 'api'], {
    shell: true,
    stdio: 'inherit',
  });

  // Store the server process in globalThis so it can be accessed in globalTeardown
  globalThis.__SERVER_PROCESS__ = server;
  // Hint: Use `globalThis` to pass variables to global teardown.
  globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';

  // You might want to wait for the server to be fully up before proceeding
  // This is a simplistic approach; consider polling a health endpoint instead
  await new Promise((resolve) => setTimeout(resolve, 5000));
};

// global-teardown.ts

module.exports = async function () {
  // Put clean up logic here (e.g. stopping services, docker-compose, etc.).
  // Hint: `globalThis` is shared between setup and teardown.
  console.log(globalThis.__TEARDOWN_MESSAGE__);

  // Stop the server process initiated in globalSetup
  if (globalThis.__SERVER_PROCESS__) {
    globalThis.__SERVER_PROCESS__.kill();
  }
};
codePassion-dot commented 7 months ago

yeah I ended up doing all the setup there, here it is maybe it could help someone:

// global-setup.tsx

import { v2 as compose } from 'docker-compose';
import * as path from 'path';
import { spawn, exec } from 'child_process';
import util from 'util';
/* eslint-disable */
var __TEARDOWN_MESSAGE__: string;

module.exports = async function () {
  // Start services that that the app needs to run (e.g. database, docker-compose, etc.).
  console.log('\nSetting up...\n');

  const execAsync = util.promisify(exec);
  const buildBackend = () => {
    return new Promise((resolve, reject) => {
      const dockerBuild = spawn('nx', ['docker-build', 'api-rest'], {
        cwd: path.join(__dirname, '../../../../'),
      });
      dockerBuild.stdout.on('data', (data) => {
        process.stdout.write(`${data}\n`);
      });
      dockerBuild.stderr.on('data', (data) => {
        process.stderr.write(`${data}\n`);
      });
      dockerBuild.on('close', (code) => {
        resolve(`Docker build process exited with code ${code}`);
      });
      dockerBuild.on('error', (err) => {
        reject(`Failed to build docker image, ${err}`);
      });
    });
  };

  const startDatabase = async () => {
    await compose.upAll({
      cwd: path.join(__dirname, '../../../../libs/db'),
      env: { UID: String(process.getuid()), GID: String(process.getgid()) },
      callback: (chunk: Buffer) => {
        console.log('job in progress: ', chunk.toString());
      },
    });
  };

  const startBackend = () => {
    return new Promise(async (resolve, reject) => {
      try {
        const { stderr, stdout } = await execAsync(
          'docker run --net host --env-file ./.env --name api-rest -d -t api-rest',
          {
            cwd: path.join(__dirname, '../../../api-rest'),
          }
        );
        console.log(`stdout: ${stdout}`);
        console.error(`stderr: ${stderr}`);
        // NOTE: This is a workaround to give the container time to boot up.
        setTimeout(resolve, 2000);
      } catch (error) {
        reject(error);
      }
    });
  };

  try {
    await startDatabase();
    const buildBackendResponse = await buildBackend();
    console.log(buildBackendResponse);
    await startBackend();
  } catch (err) {
    console.log(
      'Something went wrong during docker compose boot-up:',
      err.message
    );
  }
  // Hint: Use `globalThis` to pass variables to global teardown.
  globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';
};

// global-teardown.ts

/* eslint-disable */

import path from 'path';
import { exec } from 'child_process';
import util from 'util';
import { v2 as compose } from 'docker-compose';

module.exports = async function () {
  // Put clean up logic here (e.g. stopping services, docker-compose, etc.).
  // Hint: `globalThis` is shared between setup and teardown.
  const execAsync = util.promisify(exec);
  const shutdownBackend = async () => {
    const { stderr: stderrBackendStop, stdout: stdoutBackendStop } =
      await execAsync('docker stop api-rest', {
        cwd: path.join(__dirname, '../../../api-rest'),
      });
    console.log(`stdout: ${stdoutBackendStop}`);
    console.error(`stderr: ${stderrBackendStop}`);
    const { stderr: stderrBackendRemove, stdout: stdoutBackendRemove } =
      await execAsync('docker container rm api-rest', {
        cwd: path.join(__dirname, '../../../api-rest'),
      });
    console.log(`stdout: ${stdoutBackendRemove}`);
    console.error(`stderr: ${stderrBackendRemove}`);
  };
  const stopDatabase = async () => {
    await compose.down({
      cwd: path.join(__dirname, '../../../../libs/db'),
      commandOptions: [['--volumes'], ['--remove-orphans'], ['-t', '1']],
      env: { UID: String(process.getuid()), GID: String(process.getgid()) },
      callback: (chunk: Buffer) => {
        console.log('job in progress: ', chunk.toString());
      },
    });
  };
  try {
    await stopDatabase();
    console.log('Database is shutdown.');
    await shutdownBackend();
  } catch (err) {
    console.log(
      'Something went wrong during docker compose shutdown:',
      err.message
    );
  }
  console.log(globalThis.__TEARDOWN_MESSAGE__);
};
kamau-crypto commented 7 months ago

I believe it's already provided. @john-james-gh , Your solution worked perfectly for me 👍. However, it can be improved to format the output on the console/terminal and make it readable by changing the stdio: 'inherit' to stdio:'pipe' . Read more on this from nodejs child process

// jest.config.ts

export default {
  displayName: 'api-e2e',
  globalSetup: '<rootDir>/src/support/global-setup.ts',
  globalTeardown: '<rootDir>/src/support/global-teardown.ts',
  ...
};

// global-setup.ts

import { spawn } from 'child_process';

module.exports = async function () {
  // Start services that that the app needs to run (e.g. database, docker-compose, etc.).
  console.log('\nSetting up...\n');

  // Start the API server
  const server = spawn('nx', ['serve', 'api'], {
    shell: true,
    //stdio: 'inherit',
    stdio:'pipe',
  });

  // Store the server process in globalThis so it can be accessed in globalTeardown
  globalThis.__SERVER_PROCESS__ = server;
  // Hint: Use `globalThis` to pass variables to global teardown.
  globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';

  // You might want to wait for the server to be fully up before proceeding
  // This is a simplistic approach; consider polling a health endpoint instead
  await new Promise((resolve) => setTimeout(resolve, 5000));
};

// global-teardown.ts

module.exports = async function () {
  // Put clean up logic here (e.g. stopping services, docker-compose, etc.).
  // Hint: `globalThis` is shared between setup and teardown.
  console.log(globalThis.__TEARDOWN_MESSAGE__);

  // Stop the server process initiated in globalSetup
  if (globalThis.__SERVER_PROCESS__) {
    globalThis.__SERVER_PROCESS__.kill();
  }
};
github-actions[bot] commented 6 days ago

This issue has been automatically marked as stale because it hasn't had any activity for 6 months. Many things may have changed within this time. The issue may have already been fixed or it may not be relevant anymore. If at this point, this is still an issue, please respond with updated information. It will be closed in 21 days if no further activity occurs. Thanks for being a part of the Nx community! 🙏