Testing

A quickstart on writing tests for your Retool apps.

This feature is in limited release, so you might not see it in Retool. If you're interested in access, please email [email protected]

Retool's testing feature enables you to write unit, end-to-end, and integration tests for your application. You write tests by using APIs provided by Retool to manipulate components, and by using helper functions to assert conditions. This guide will walk you through writing a few simple tests.

Sample end-to-end test

Sample application with modal to add new users.Sample application with modal to add new users.

Sample application with modal to add new users.

We'll write an end-to-end test for our form to ensure that it can correctly add new users when it's submitted.

Retool editor showing the menu to write new tests.Retool editor showing the menu to write new tests.

Retool editor showing the menu to write new tests.

To open the tests modal, navigate to the tests option in the top right menu of Retool. We'll use the editor inside of the menu to start writing our test.

When we write a test, we want to use methods on components in Retool to change the state of the application and then make assertions against the state. To write a test submitting the form, we'll open the modal that contains the form, fill out each textInput, then submit the form.

First, we'll write a test that verifies the form is not enabled until all of its fields are completed.

await modal1.open()

assertCondition(form1.submitDisabled)
await userName.setValue('Retool User')

assertCondition(form1.submitDisabled)
await creditScore.setValue('Credit Score')

assertCondition(form1.submitDisabled)
await accountBalance.setValue('Account Balance')

assertCondition(!form1.submitDisabled)

If you're ever unsure of what methods you can use, type the name of the component and look at the options in the auto complete. We have methods for most changes you'd want to make and values you'd want to access, and we're happy to add more if you don't see methods you need!

Make sure to await each action you perform so your test continues after the action is finished and your Retool app is updated.

In this test, we use assertCondition on a property of the form (form1.submitDisabled in this case!) to ensure that our application has the state we expect it to after each step of the test. We can use assertCondition to assert any expression that we expect to be true after state is updated.

Sometimes, however, assertCondition won't work because we want to wait for a change that happens outside of our application (like if we hit an API and want to wait for its response). To wait for external changes, we'll use waitForCondition. Here's an extension of the test that waits for our form the submit correctly and trigger an API response:

await modal1.open()

assertCondition(form1.submitDisabled)
await userName.setValue('Retool User')

assertCondition(form1.submitDisabled)
await creditScore.setValue('Credit Score')

assertCondition(form1.submitDisabled)
await accountBalance.setValue('Account Balance')

assertCondition(!form1.submitDisabled)

await form1.submit()
await waitForCondition(() => addNewUser.data.Name === 'Retool User')

In the example above, submitting the form triggers an API that will update addNewUser.data. The waitForCondition function defaults to waiting for 10 seconds for a condition to become true.

Sample unit test

We can also write unit tests that test business logic inside of Retool applications. We'll write a unit test for a transformer that sums all of the account balances in our table and displays the result.

Transformer that sums the value of all account balances inside of our table.Transformer that sums the value of all account balances inside of our table.

Transformer that sums the value of all account balances inside of our table.

To write a unit test for our table, we'll want to change the data inside of the table. Note that this gives you the flexibility to set the data in your table to whatever you'd like, so you can test different scenarios. The transformer will automatically be run after the table's value changes and we can assert against its value. Here's a sample test that checks if the transformer sums to the correct value:

await table1.setData([
  {'Account balance (thousands)': '$1'},
  {'Account balance (thousands)': '$3'},
  {'Account balance (thousands)': '$10'},
])
assertCondition(sumAccounts.value === 14)

If you're ever unsure what the shape of the data that you want to write a test for is, check the left hand panel and look at the data currently in the field you want to set. Getting the shape of data right is one of the hardest parts of Retool!

Writing a mock

Often, when you're writing a test, you do not want to hit a live endpoint. You can stop all queries and network calls from firing during the execution of a test to ensure that you never accidentally write to a resource you don't want to.

Checkbox used to mock all queriesCheckbox used to mock all queries

Checkbox used to mock all queries

By default, any query fired when network calls are disabled will return an empty JSON. If you want to customize responses for queries to have consistent, static data in your tests, you can use the mock function. The mock function takes the name of the query and the value you'd like to return (example below). Mocks are only valid for queries triggered while the test is being executed; you do not need to reset mocks at the end of the test.

Here's a sample test that mocks query1:

await mock('query1', {foo: 'bar'})
await query1.trigger()
assertCondition(query1.data['foo'] === 'bar')

More sample tests

Test that a button fires a query

Say you have a button1 that fires query1 when it is a clicked and you want to ensure that piece of functionality in a test (but don't actually want to hit the query1 endpoint). Here is a sample tests that tests if a button click fires a query:

var mockResponse = {foo: 'bar'}
await mock('query1', mockResponse)
await eventTrigger.click('button1')
await assertQueryCalledTimes('query1', 1)
await assertEquals(query1.data.foo, 'bar')

Notice the eventTriggers API that lets you trigger any event handler on any component, the assertEquals method that lets you compare 2 values, and the assertQueryCalledTimes method that lets you check how many times a query was called. Here's how the test from above would look in the testing modal:

More about tests

Execution of tests

Every time a test is run, the app is reset and loaded at its initial state. This means that all queries that run on page load/model updates are fired and the test simulates exactly what the user sees when they load an app. As the test is executed, its calls manipulate the model and state of the Retool app by simulation user actions and it can contain assertions against the model to confirm desired behavior.

The app resets between runs of different tests and once you close the testing modal, the app returns to its original state as well.

Reference: Testing Methods

assertCondition(someCondition: boolean)

Returns successfully if someCondition is true, throws an error if it is not.

assertEquals(actual: {}, expected: {})

Returns successfully if actual is equal to expected, throws an error otherwise.

await assertQueryCalled(queryId: string)

Returns successfully if the query with the given queryId is called, throws an error if it is not.

await assertQueryCalledTimes(queryId: string, callNumber: number)

Returns successfully if the query with the given queryId is called exactly callNumber times, throws an error if it is not.

await waitForCondition(someFunction: () => boolean, options?: {timeout?: number})

Evaluates someFunction every half second for timeout milliseconds, returns successfully if it ever returns true and fails if it does not. The default timeout is 10000 milliseconds.

await eventTriggers.event(componentId: string)

Triggers event on the component with the given componentId (example: await eventTriggers.click('button1')).

await mock(queryId: string, returnObject: {})

Mocks the query with the given queryId to always return returnObject.

For methods that manipulate components (i.e. textInput1.setValue('foo') or table1.selectRow(0)), see Scripting Retool


Did this page help you?