Running unit tests using Typescript (excluding AngularJs) can be accomplished by incorporating Jasmine and Webpack

While there is an abundance of resources on how to unit test with Webpack and Jasmine for Angular projects, I am working on a project that utilizes 'plain' TypeScript instead of AngularJs.

I have TypeScript classes in my project but do not use components. I am struggling to apply the information I find to a non-AngularJs project as everything seems to be focused on using components.

How can I integrate Jasmine (ts spec files) into a Typescript Webpack project?

I would prefer a solution that involves using a separate webpack configuration for the tests.

My setup/what I have to work with:

package.json I use a start script that launches node with dev-build

{
  "name": "webpack.typescript",
  "version": "1.0.0",
  "description": "Webpack + TypeScript",
  "main": "dev-build.js",
  "author": "Shane Osbourne and John Lindquist",
  "license": "MIT",
  "scripts": {
    "start": "node dev-build"
  },
  "dependencies": {
    "bootstrap": "^3.3.7",
    "lodash": "^4.17.4"
  },
  "devDependencies": {
    "@types/lodash": "^4.14.53",
    "browser-sync": "^2.18.8",
    "bs-pretty-message": "^1.0.8",
    "css-loader": "^0.26.2",
    "node-sass": "^4.5.0",
    "sass-loader": "^6.0.2",
    "style-loader": "^0.13.2",
    "ts-loader": "^2.0.1",
    "typescript": "^2.2.1",
    "url-loader": "^0.5.8",
    "webpack": "^2.2.1",
    "webpack-dev-middleware": "^1.10.1"
  }
}

dev-build.js This file loads config, bundles the code, and starts BrowserSync.

/**
 * Require Browsersync along with webpack and middleware for it
 */
var browserSync          = require('browser-sync').create();
var webpack              = require('webpack');
var webpackDevMiddleware = require('webpack-dev-middleware');

/**
 * Require ./dev-webpack.config.js and make a bundler from it
 */
var webpackConfig = require('./dev-webpack.config');
var bundler       = webpack(webpackConfig);

/**
 * Reload all devices when bundle is complete or send a fullscreen error message to the browser instead
 */
bundler.plugin('done', function (stats) {
    if (stats.hasErrors() || stats.hasWarnings()) {
        return browserSync.sockets.emit('fullscreen:message', {
            title: "Error",
            timeout: 100000
        });
    }
    browserSync.reload();
});

/**
 * Run Browsersync and use middleware for Hot Module Replacement
 */
browserSync.init({
    server: 'app',
    open: false,
    logFileChanges: false,
    middleware: [
        webpackDevMiddleware(bundler, {
            publicPath: webpackConfig.output.publicPath,
            stats: {colors: true}
        })
    ],
    plugins: ['bs-pretty-message'],
    files: [
        'app/css/*.css',
        'app/*.html'
    ]
});

dev-webpack.config.js This file handles typescript and scss. (I was thinking maybe I should also have a test-webpack.config.js)

var webpack = require('webpack');
var path = require('path');

module.exports = {
  devtool: '#inline-source-map',

  entry: [
    './src/main.ts',
    './src/main.scss'
  ],

  output: {
    path: path.join(__dirname, 'app'),
    publicPath: '/',
    filename: 'dist/bundle.js'
  },

  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.LoaderOptionsPlugin({
      debug: true
    })
  ],

  resolve: {
    extensions: ['.ts', '.js', '.scss']
  },

  module: {
    rules: [
      {
        test: /\.ts$/, 
        use: [{
          loader: 'ts-loader'
        }]
      },
      {
        test: /\.woff($|\?)|\.woff2($|\?)|\.ttf($|\?)|\.eot($|\?)|\.svg($|\?)/,
        use: [{
          loader: 'url-loader'
        }]
      },
      {
        test: /\.scss$/,
        use: [{
          loader: "style-loader"
        }, {
          loader: "css-loader", 
          options: {
            sourceMap: true
          }
        }, {
          loader: "sass-loader", 
          options: {
            sourceMap: true
          }
        }]
      }
    ],
  }
};

What I've come across in my search:

Unit testing with Webpack and Mocha

Unit testing with Webpack, Jasmine (-core), typescript

Jasmine Spec as Typescript File

TypeScript compilation failure and Karma test execution?

Executing Typescript Jasmine tests via Webpack (Terminal) to test Angular2

Answer №1

Although Ngz's answer was comprehensive with Angular and many extra details, I will aim to provide a simplified response.

In the test-webpack-config.js file, the tests serve as the entry point for webpack to locate dependencies, while the ts-loader handles transpilation:

var webpack = require('webpack');
var path = require('path');

module.exports = {
  devtool: '#inline-source-map',

  entry: [
    './test/index.spec.ts',
  ],

  output: {
    filename: 'dist/bundle.js'
  },

  plugins: [
    new webpack.NoEmitOnErrorsPlugin(),
    new webpack.LoaderOptionsPlugin({
      debug: true
    })
  ],

  resolve: {
    extensions: ['.ts', '.js', '.tsx']
  },

  module: {
    rules: [
      {
        test: /\.ts$/, use: [{
          loader: 'ts-loader'
        }]
      }]
  }
};

I utilized karma as the test runner, incorporating the karma-webpack plugin. Here is the configuration:

var webpackConfig = require('./test-webpack.config.js');

module.exports = function(config) {
  config.set({

    // Configuration details here...

  })
}

The significance of the mime part is crucial in ensuring that TypeScript-based tests function properly.

For a complete example, refer to: https://github.com/SierraNL/webpack-typescript-jasmine

Answer №2

To successfully run tests, you must configure a test runner to utilize a modified Webpack configuration specifically for your testing environment. In this case, Karma and the karma-webpack plugin are utilized as an example.

In order to achieve this, it is necessary to create a distinct entry file for your test bundle and specify its inclusion in the values of the files and preprocessors properties within your karma config file:

// /test/karma.conf.js
const argv = require('yargs').argv;
const isDebug = argv.debug === 'true';
const isCoverage = argv.coverage === 'true';
const path = require('path');
const webpack = require('webpack');

// Additional coverage reporting instrumentation loader
const instrumentLoader = {
    test: /\.js$|\.ts$/,
    enforce: 'post',
    use: [
        {
            loader: 'istanbul-instrumenter-loader',
            options: {
                esModules: true
            }
        }
    ],
    exclude: /node_modules/
};

const coverageReporter = {
    reporters: [
        {
            type: 'text-summary'
        }
    ]
}

module.exports = (config) => {
    config.set(Object.assign({
            browsers: [isDebug ? 'Chrome' : 'PhantomJS'],
            files: [
                // Path to test bundle entry file
                './test.entry.js'
            ],
            frameworks: ['jasmine', 'es6-shim'],
            reporters: isDebug ? ['spec'] : ['spec', 'coverage'],
            singleRun: !isDebug,
            autoWatch: isDebug,
            preprocessors: {
                // Path to test bundle entry file
                './test.entry.js': ['webpack']
            },
            webpackMiddleware: {
                stats: 'errors-only'
            }
        },
        (isCoverage ? { coverageReporter } : {}),
        {
            webpack: {
                devtool: 'eval-source-map',
                resolve: {
                    extensions: [
                        '.js',
                        '.ts',
                        '.less',
                        '.html',
                        '.json'
                    ],
                    modules: [path.resolve(process.cwd(), 'src/client'), 'node_modules']
                },
                module: {
                    rules: [
                        {
                            test: /\.js$/,
                            use: [
                                'babel-loader'
                            ],
                            exclude: /node_modules/
                        },
                        {
                            test: /\.ts$/,
                            use: [
                                'babel-loader',
                                {
                                    loader:'ts-loader',
                                    options: {
                                        entryFileIsJs: true
                                    }
                                }
                            ],
                            exclude: /node_modules/
                        }
                    ].concat(isCoverage ? [instrumentLoader] : [])
                }
            }
        }
    ));
};

Subsequently, establish the test bundle's entry point:

// /test/test.entry.js
// Utilize polyfills/vendor libraries for testing
import 'babel-core/register';
import 'babel-polyfill';

// Create a context with files matching the specified pattern
const context = require.context('./', true, /^\.\/.*\.spec\.(js|ts)$/);

// Require all files within the context
context.keys().forEach(context);

You can then execute Karma by providing the path to the karma.conf.js file along with parameters specifying --debug and --coverage:

$ karma start ./test/karma.conf.js --coverage=true --debug=false

Alternatively, include these commands in the scripts property of your package.json:

// package.json
{
    ...
    "scripts": {
        "test": "cross-env karma ./test/karma.conf.js",
        "test:watch": "cross-env karma ./test/karma.conf.js --debug=true",
        "test:coverage": "cross-env karma ./test/karma.conf.js --coverage=true",
    }
    ...
}

Invoke these scripts using npm: npm run test

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The ReactDOM.createPortal modal has been successfully mounted onto the DOM, however, there seems to be no visible content

This project utilizes TypeScript and Next.js. Within it, there is a Modal component: interface ModalProps { onCancelModal: () => void; onAcceptModal: () => void; acceptEnabled: boolean; isLoading?: boolean; title: string; } const Modal: Re ...

Difficulties in Networking Requests Following Event Emitter Notification in an Angular Application

Within my Angular application, a network request is sent to retrieve filtered data based on user-selected filters. The function responsible for handling the filter values and executing the request is outlined as follows: public onFilterReceived(values) { ...

Retrieve the data from a JSON file using Angular 4

I have a JSON data structure that looks like this: [{"id": "ARMpalmerillas07", "type": "GreenHouse","act_OpenVentanaCen": {"type": "float", "value": 0, "metadata": {"accuracy": {"type": "Float", "value": "07/02/2018 13:08 : 43 "}}}, "act_OpenVentanaLatNS" ...

What is the solution for resolving this Angular issue: Expected argument expression.ts(1135)?

While following a CRUD tutorial, I encountered an issue with the code. Even though I have verified that my code matches the tutorial's code, I am getting an error message saying "Argument expression expected. ts(1335)" in the submit method onSubmit(). ...

Tips for resolving the error of encountering a forbidden and unexpected token in JSON at position 0 while using React hooks

const [forecastData, setForecastData] = useState({ forecast: []}); useEffect(() => { let ignore = false; const FETCHDATA = async () => { await fetch(forecast,{ headers : { ...

Opening a modal from a different component in Angular 6

I am attempting to launch a modal that is associated with a separate component. However, I encountered an error ERROR TypeError: Cannot read property 'show' of undefined Here is my code: product-catalog.component.html <app-cart-table-modal& ...

The movement of particles in tsparticles experiences interruptions when built in React, causing defects in their motion or noticeable stutter and lag

The performance is flawless in development mode with npm run start, but once deployed and running the production build (npm run build), there seems to be a disturbance in particle movement or a drastic decrease in speed. Despite experimenting with all ava ...

Is it possible to use a single type predicate for multiple variables in order to achieve type inference?

Is there a way to optimize the repeated calls in this code snippet by applying a map to a type predicate so that TSC can still recognize A and B as iterables (which Sets are)? if(isSet(A) && isSet(B)) { ...

Creating an HTML tag from Angular using TypeScript

Looking at the Angular TypeScript code below, I am trying to reference the divisions mentioned in the HTML code posted below using document.getElementById. However, the log statement results in null. Could you please advise on the correct way to reference ...

Aliases for NPM packages and TypeScript declaration files

I am in need of two separate versions of a package, and fortunately with npm 6.9.0 I can easily accomplish that now. My dilemma is this: the package comes with type definitions. However, when I create an alias for this package and refer to it using the al ...

Angular Authentication Functionality

I need to create a loggedIn method in the AuthService. This method should return a boolean indicating the user's status. It will be used for the CanActivate method. Here is a snippet of code from the AuthService: login(email: string, password: string) ...

How can nested json be sorted effectively based on two specific fields?

Example Data: [{ 'ID': objID(abc123), 'Department': 'IT', 'Employees': [ { 'ID': 3, 'StartDate': '24-12-2022T08:30', 'active': true }, { ...

Differences between React Typescript: @types/react-router-dom and react-router-dom

Hello there! This is my first time working with Typescript in a React project, and I'm feeling a bit confused about the @types npm packages. Can someone explain the difference between @types/react-router-dom and react-router-dom, as well as suggest wh ...

Transitioning from using $http to Restangular has caused issues with our tests now not passing

I am currently working with the following controller: 'use strict'; angular.module('App') .controller('LoginCtrl', function ($scope, $state, Login, localStorageService, Message, authService) { $scope.submit = function ...

TypeScript excels in typechecking when using two individual assignments, but may encounter issues when attempting typechecking with tuple

I am quite puzzled by a discovery I made and I am seeking to understand why TypeScript is displaying this behavior and what the underlying reason may be. Here is the code snippet: class A { constructor(public name : String, public x = 0, public y = 0) ...

Switch between active tabs (Typescript)

I am working with an array of tabs and here is the code snippet: const navTabs: ITab[] = [ { Name: allTab, Icon: 'gs-all', Selected: true }, { Name: sources.corporateResources, Icon: 'gs-resources', Selected: false }, { Name ...

Are there any more efficient methods to retrieve an object from an arrow function in TypeScript?

Trying to retrieve an object from an arrow function is posing a challenge for me, especially with the following function f: myMethod(f: data => { return { someField: data.something }; }); I am aware that for simple types, you can condense the arrow ...

Encountering numerous issues during my attempt to perform an npm install command

After cloning a git repository, I encountered an issue when trying to run the app in the browser. Despite running "npm install," some dependencies were not fully installed. Upon attempting to run "npm install" again, the following errors were displayed: np ...

Instead of returning an object, the underscore groupBy function now returns an array

Currently, I am attempting to utilize underscore to create an array of entities that are grouped by their respective locations. The current format of the array consists of pairs in this structure { location: Location, data: T}[]. However, I aim to rearran ...

Refining types in a series of statements

I'm attempting to merge a series of assertions in a safe manner without the need to individually call each one. For instance, consider the following code: type Base = { id: string, name?: string, age?: number }; type WithName = { name: string }; type ...