Skip to content
Search

    Testing

    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.

    Preliminaries

    Add optional dependencies required by cds.test:

    npm add -D axios chai chai-as-promised chai-subset jest
    

    If you have cloned cap/samples, you get that for free.

    Running a CAP Server

    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/lib')
    cds.test(project)
    

    Learn more about tests in the SFLIGHT app.

    Behind the Scenes cds.test()
    • 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
    Running in a Specific Folder

    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/lib')
    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.

    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.

    Testing Service APIs

    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)))
    })
    

    Testing HTTP APIs

    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', {
      params: { $search: 'Po', $select: `title,author`
    }})
    

    Learn more about the axios APIs.

    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`, {
      book: 201, quantity: 5
    })
    const { data } = await GET ('/browse/Books', {
      params: { $search: 'Po', $select: `title,author`
    }})
    

    Using Mocha or Jest

    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.

    All tests in cap/samples are written in that portable way.
    Run them with npm run jest or with npm run mocha.

    Using Watchers

    You can also start the tests in watch mode, for example:

    jest --watchAll
    

    which 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.

    Using cds.test in REPL

    You can use cds.test in REPL, for example, by running this from your command line:

    [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' }
    ]
    

    Providing Test Data

    Data can be supplied:

    • Programmatically as part of the test code
    • In CSV files from db/data folders

    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')
      const {Books} = db.model.entities('my.bookshop')
      await db.create(Books).entries([
        {ID:401, title: 'Book 1'},
        {ID:402, title: 'Book 2'}
      ])
      // 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.

    Data Reset

    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 test
    

    or reset it whenever needed:

    await data.reset()
    

    or only delete it:

    await data.delete()
    

    cds.test Reference

    cds.test (projectDir) Test

    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.

    cds.test (command, …args) Test

    Launch a server with the given command and arguments.
    Example: cds.test ('serve', '--in-memory', '--project', <dir>)

    class Test

    Instances of this class are returned by cds.test(). See below for its functions and properties.

    .GET/PATCH/POST/PUT/DELETE (url, …) response

    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 `/foo` and GET(`/foo`) are equivalent.

    .expect expect

    Provides the expect function from the chai assertion library.

    .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.

    .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.

    .data { }

    Provides utilities to manage test data:

    • .autoReset (boolean) enables automatic deletion and redeployment of CSV data before each test. Default is false.
    • .delete () deletes data in all database tables
    • .reset () deletes data in all database tables and deploys CSV data again

    .in (…paths) Test

    Sets the given path segments as project root.
    Example: cds.test.in(__dirname, '..').run('serve', '--in-memory')

    .verbose (boolean) Test

    Sets verbose mode, so that, for example, server logs are shown.

    Best Practices

    Check Response Data Instead of Status Codes

    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 this
    

    This 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.

    Be Relaxed When Checking Error Messages

    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