Skip to content

Testing

Daniel O'Grady edited this page Sep 10, 2024 · 4 revisions

🚧 This page is still work in progress!

When contributing to cds-typer, providing a suitable set of unit tests is highly appreciated.

There are two types of tests available: precise 🌳 AST Tests and 🧩 Compilation Tests, that are easier to write. The type of tests you should use depends on the feature you are building for cds-typer.

🌳 AST Tests

This type of tests generates types for a test model you provide and inspects the AST of the generated .ts file. These tests give you very finely grained access to syntax tokens of the generated files. For example, when you want to check if a property in a generated type has a certain modifier, you would write an AST test.

💡 Use when

  • you want to ensure a certain structure (e.g. does the generated file contain the classes A, B, C? Does C have a property d of type number?)
  • you need to inspect certain modifiers, especially ones that don't really manifest during compilation (e.g. is a certain property marked as static?).
  • you want to write methodic checks (e.g. does every property in a generated class start with a certain character?)

Most tests in the current suites follow the same structure:

  1. define a model that exposes certain traits of CDS we want to the cds-typer against
  2. generate the types for this model using cds-typer
  3. parse the resulting files into a TypeScript AST
  4. inspect the AST to confirm the generated code meets all expectations

The following section explores the process of writing new tests in detail.

Writing a Test Model

Let's assume you want to support some feature "X" from CDS. To make sure X can be tested in isolation you will start with creating a test model in test/unit/files/X/model.cds with a minimal model that focuses on X. Make sure the model is as brief and self-contained as possible.

Also, create a Jest testfile test/X.test.js. You can transfer the general structure from one of the existing test files.

Programmatically Generating Types for Testing

For most tests you will compile the test model once and then test several assumptions about the generated files through multiple test cases. The compilation of your model can be trigger through the compileFromFile method from the cds-typer core module. To make sure the tests run smoothly on all operating systems as well as in the CI pipeline, you should use the following utility functions:

util.locations.unit.files(m) will resolve the passed model m from the unit test directory to avoid any path confusion util.locations.testOutput(m) creates a path for a temporary directory for the test suite m to generate files into

const { compileFromFile } = require('../../lib/compile')
const { ASTWrapper } = require('../ast')
const { locations } = require('../util')

const paths = await compileFromFile(locations.unit.files('X/model.cds'), { 
    outputDirectory: locations.testOutput('X') 
})

Parsing Generated Types for Further Inspection

Calling compileFromFile will return a list of paths pointing to the files that were generated in the process. Note that there are certainly more elegant ways to retrieve one particular file than using a hard coded index.

Once you have determined the file you want to inspect, you can wrap them in an ASTWrapper. There are two wrappers available:

ASTWrapper for TypeScript Files

This is the wrapper you are mainly working with for testing. It uses the native TypeScript compiler under the hood to load a given .ts file and parse it into an abstract syntax tree (AST). The wrapper class simplifies the structure of the original TS AST, which is quite verbose. This makes certain nodes significantly more accessible by removing some layers of indirection of enriching nodes with useful information. You can produce such a wrapped AST as follows:

let astw
beforeAll(async () => {
    const paths = await cds2ts.compileFromFile(locations.unit.files('X/model.cds'), { ... })
    astw = new ASTWrapper(path.join(paths[1], 'index.ts'))
}

The downside of this process is that the wrapped AST only contains the nodes that are explicitly visited! If you are contributing a feature that causes cds-typer to emit any TypeScript construct that has not been emitted before, chances are that the corresponding TypeScript AST nodes are simply ignored by the wrapper class. In that case you have to adjust the wrapper accordingly.

JSASTWrapper for JavaScript Files

cds-typer mainly emits TypeScript files, accompanied by a thin JavaScript wrapper around cds.entities(...) to make sure all imports work during runtime as well. The way these JavaScript files are generated will rarely change. But if you contribute a feature that causes a change in the JavaScript files, you should test them as well. For them you can use the JSASTWrapper.

let jsastw
beforeAll(async () => {
    const paths = await cds2ts.compileFromFile(locations.unit.files('X/model.cds'), { ... })
    jsastw = await JSASTWrapper.initialise(path.join(paths[1], 'index.js'))
}    

Note the use of the .initialise factory due to the asynchronous nature of the process of reading in the JS files. The resulting wrapper uses the AST produced by Acorn internally. While the wrapper already comes with a few convenience methods, you can inspect the Acorn AST freely to make sure the generated JavaScript files match your expectations.

🧩 Compilation Tests

This type of tests is a bit less involved and easier to conceive, as you don't have to dive into the structure of the AST and the helper methods used to inspect it. Instead, you just write consumer code for the generated types. This code is then transpiled using tsc and checked for consistency on type level.

💡 Use when

  • you want to make sure the generated code is consistent
  • you can conceive TypeScript code that enforces the generated code to be correct on type level, for example an assignment of a property to a typed variable, which will only work when you generated the property correctly

Writing Consumer Code

In the test folder, create an appropriately named folder for the CDS feature you are providing types for[^1]. Provide a model.cds file that contains the CDS feature. In the same folder, create a model.ts that uses the generated types. Provide a model.ts file that illustrates how the feature you are providing/ fixing is supposed to work. You can check an example usage in the existing tests.

[^1]: if there is already an appropriate folder, you can just move in there.