| shorty | cds.test |
|---|---|
| status | released |
Learn more about best practices to test CAP Node.js applications using the cds.test toolkit.
Find samples for such tests in cap/samples and in CAP SFLIGHT app.
For more details how to test Java applications, see the Java documentation.
Add optional dependencies required by cds.test:
npm add -D axios chai chai-as-promised chai-subset jest::: tip If you have cloned cap/samples, you get that for free. :::
Use function cds.test() to easily launch and test a CAP server as follows:
const project = __dirname+'/..' // The project's root folder
const cds = require('@sap/cds')
cds.test(project)
Learn more about tests in the SFLIGHT app.{.learn-more}
- Ensures the server is launched before tests (→ in
before()/beforeAll()hooks) - With the equivalent of
cds serve --project <...> --in-memory?cli command - With a controlled shutdown when all tests have finished
By default, the cds APIs read files from the current working directory. To run test simulating whole projects, use cds.test.in(<...>) to specify the test project's root folder.
const cds = require('@sap/cds')
cds.test.in(__dirname)
For example, this would have cds.env loading the configuration from package.json and .cdsrc.json files found next to the test file, that is, in the same folder.
::: danger
Important: Don't use process.chdir() in Jest tests, as they may leave test containers in screwed state, leading to failing subsequent tests.
:::
::: tip
Prefer using relative filenames derived from __dirname as arguments to cds.test to allow your tests be started from whatever working directory.
:::
To reduce noise, cds.test() by default suppresses the usual bootstrap output of cds serve. You can skip this silent mode programmatically like that:
cds.test(project).verbose()Or by setting process env variable CDS_TEST_VERBOSE, for example like that from the command line:
::: code-group
CDS_TEST_VERBOSE=y mochaset CDS_TEST_VERBOSE=y
mocha$Env:CDS_TEST_VERBOSE=y
mocha:::
To get a completely clutter-free log, check out the test runners for such a feature, like jest --silent.
As cds.test() launches the server in the current process, you can access all services programmatically using the respective Node.js APIs.
Here is an example for that taken from cap/samples:
it('Allows testing programmatic APIs', async () => {
const AdminService = await cds.connect.to('AdminService')
const { Authors } = AdminService.entities
expect (await SELECT.from(Authors))
.to.eql(await AdminService.read(Authors))
.to.eql(await AdminService.run(SELECT.from(Authors)))
})To test HTTP APIs we can use the test object returned by cds.test(), which uses axios and mirrors the axios API methods like .get(), .put(), .post() etc.
const test = cds.test('@capire/bookshop')
const {data} = await test.get('/browse/Books', { // [!code focus]
params: { $search: 'Po', $select: `title,author`
}})
Learn more about the axios APIs.{.learn-more}
In addition we provide uppercase bound function variants like GET or POST, which allow this usage variant:
const { GET, POST } = cds.test('@capire/bookshop')
const input = 'Wuthering Heights' // simulating user input
const order = await POST (`/browse/submitOrder`, { // [!code focus]
book: 201, quantity: 5
})
const { data } = await GET ('/browse/Books', { // [!code focus]
params: { $search: 'Po', $select: `title,author`
}})
Mocha and Jest are the most used test runners at the moment, with each having its fan base.
The cds.test library is designed to write tests that run with both, as shown in the following sample code:
const { GET, expect } = cds.test('@capire/bookshop')
describe('my test suite', ()=>{
beforeAll(()=>{ }) // Jest style
before(()=>{ }) // Mocha style
test ('something', ()=>{}) // Jest style
it ('should test', ()=>{ // Jest & Mocha style
const { data } = await GET ('/browse/Books', {
params: { $search: 'Po', $select: `title,author` }
})
expect(data.value).to.eql([ // Chai tests, working in Jest and Mocha
{ ID: 201, title: 'Wuthering Heights', author: 'Emily Brontë' },
{ ID: 207, title: 'Jane Eyre', author: 'Charlotte Brontë' },
{ ID: 251, title: 'The Raven', author: 'Edgar Allen Poe' },
{ ID: 252, title: 'Eleonora', author: 'Edgar Allen Poe' },
])
})
})To be portable, you need to use a specific implementation of expect, like the one from chai provided through cds.test(), as shown in the previous sample.
You can use Mocha-style before/after or Jest-style beforeAll/afterAll in your tests, as well as the common describe, test, it methods.
::: tip
All tests in cap/samples are written in that portable way.
Run them with npm run jest or with npm run mocha.
:::
You can also start the tests in watch mode, for example:
jest --watchAllwhich should give you green tests, when running in cap/samples root:
PASS test/cds.ql.test.js PASS test/hierarchical-data.test.js PASS test/hello-world.test.js PASS test/messaging.test.js PASS test/consuming-services.test.js PASS test/custom-handlers.test.js PASS test/odata.test.js PASS test/localized-data.test.js Test Suites: 8 passed, 8 total Tests: 65 passed, 65 total Snapshots: 0 total Time: 3.611 s, estimated 4 s Ran all test suites.
Similarly, you can use other test watchers like mocha -w.
You can use cds.test in REPL, for example, by running this from your command line:
[samples](https://github.com/sap-samples/cloud-cap-samples) cds repl
Welcome to cds repl v5.5.0> cds.test('@capire/bookshop')
Test {}[cds] - model loaded from 6 file(s):
./bookshop/db/schema.cds
./bookshop/srv/admin-service.cds
./bookshop/srv/cat-service.cds
./bookshop/app/services.cds
./../../cds/common.cds
./common/index.cds
[cds] - connect to db > sqlite { database: ':memory:' }
> filling sap.capire.bookshop.Authors from ./bookshop/db/data/sap.capire.bookshop-Authors.csv
> filling sap.capire.bookshop.Books from ./bookshop/db/data/sap.capire.bookshop-Books.csv
> filling sap.capire.bookshop.Books.texts from ./bookshop/db/data/sap.capire.bookshop-Books_texts.csv
> filling sap.capire.bookshop.Genres from ./bookshop/db/data/sap.capire.bookshop-Genres.csv
> filling sap.common.Currencies from ./common/data/sap.common-Currencies.csv
> filling sap.common.Currencies.texts from ./common/data/sap.common-Currencies_texts.csv
/> successfully deployed to sqlite in-memory db
[cds] - serving AdminService { at: '/admin', impl: './bookshop/srv/admin-service.js' }
[cds] - serving CatalogService { at: '/browse', impl: './bookshop/srv/cat-service.js' }
[cds] - server listening on { url: 'http://localhost:64914' }
[cds] - launched at 9/8/2021, 5:36:20 PM, in: 767.042ms
[ terminate with ^C ]
> await SELECT `title` .from `Books` .where `exists author[name like '%Poe%']`
[ { title: 'The Raven' }, { title: 'Eleonora' } ]> var { CatalogService } = cds.services
> await CatalogService.read `title, author` .from `ListOfBooks`
[
{ title: 'Wuthering Heights', author: 'Emily Brontë' },
{ title: 'Jane Eyre', author: 'Charlotte Brontë' },
{ title: 'The Raven', author: 'Edgar Allen Poe' },
{ title: 'Eleonora', author: 'Edgar Allen Poe' },
{ title: 'Catweazle', author: 'Richard Carpenter' }
]Data can be supplied:
- Programmatically as part of the test code
- In CSV files from
db/datafolders
This following example shows how data can be inserted into the database using regular CDS service APIs (using CQL INSERT under the hood):
beforeAll(async () => {
const db = await cds.connect.to('db') // [!code focus]
const {Books} = db.model.entities('my.bookshop') // [!code focus]
await db.create(Books).entries([ // [!code focus]
{ID:401, title: 'Book 1'}, // [!code focus]
{ID:402, title: 'Book 2'} // [!code focus]
])
// verify new data through API
const { data } = await GET `/catalog/Books`
expect(data.value).to.containSubset([{ID: 401}, {ID: 402}])
})This example also demonstrates the difference of accessing the database or the service layer: inserting data through the latter would fail because CatalogService.Books is read-only. In contrast, accessing the database as part of such test fixture code is fine. Just keep in mind that the data is not validated through your custom handler code, and that the database layer, that is, the table layout, is no API for users.
Using the cds.test.data API, you can have all data deleted and redeployed before each test:
const { GET, expect, data } = cds.test ('@capire/bookshop')
data.autoReset(true) // delete + redeploy from CSV before each testor reset it whenever needed:
await data.reset()or only delete it:
await data.delete()Launches a CDS server with an arbitrary port and returns a subclass which also acts as an Axios lookalike, providing methods to send requests.
Launch a server in the given project folder, using a default command of cds serve --in-memory?.
The server is shut down after all tests have been executed.
Launch a server with the given command and arguments.
Example: cds.test ('serve', '--in-memory', '--project', <dir>)
Instances of this class are returned by cds.test(). See below for its functions and properties.
.GET/PATCH/POST/PUT/DELETE (url, ...) ⇢ response { #get}
Aliases for corresponding get/patch/... methods from Axios. For calls w/o additional parameters, a simplified call style is available where the () can be omitted. For example,
GET /fooGET(/foo) are equivalent. {.indent}
.expect → expect { #expect}
Provides the expect function from the chai assertion library. {.indent}
.chai → chai { #chai}
Provides the chai assertion library.
It is preconfigured with the chai-subset and chai-as-promised plugins. These plugins contribute the containSubset and eventually APIs, respectively. {.indent}
.axios → axios { #axios}
Provides the Axios instance that is used as HTTP client.
It comes preconfigured with the base URL of the running server, that is, http://localhost:.... This way, you only need to specify host-relative URLs in tests, like /catalog/Books. {.indent}
Provides utilities to manage test data: {.indent}
.autoReset (boolean)enables automatic deletion and redeployment of CSV data before each test. Default isfalse..delete ()deletes data in all database tables.reset ()deletes data in all database tables and deploys CSV data again {.indent}
Sets the given path segments as project root.
Example: cds.test.in(__dirname, '..').run('serve', '--in-memory') {.indent}
Sets verbose mode, so that, for example, server logs are shown. {.indent}
Avoid checking for single status codes. Instead simply check the response data:
const { data, status } = await GET `/catalog/Books`
expect(status).to.equal(200) // <-- DON'T
expect(data.value).to.containSubset([{ID: 1}]) // just do thisThis makes a difference in case of an error: with the status code check, your test will abort with a useless Expected: 200, received: xxx error, while without it, it fails with a richer error that includes a status text.
Note that by default, Axios yields errors for status codes < 200 and >= 300. This can be configured, though.
When expecting errors, compare their text in a relaxed fashion. Don't hard-wire the exact error text, as this might change over time, breaking your test unnecessarily.
await expect(POST(`/catalog/Books`,{ID:333})).to.be.rejectedWith(
'Entity "CatalogService.Books" is read-only') // DON'T hard-wire entire texts
await expect(POST(`/catalog/Books`,{ID:333})).to.be.rejectedWith(
/read?only/i) // better: check for the essential information, use regexes