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.
Interested in Continuous Integration for Retool tests?
Check out the docs on configuring CI!
Sample end-to-end test
We'll write an end-to-end test for our form to ensure that it can correctly add new users when it's submitted.
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.
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.
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 eventTriggers.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)
assertCondition(someCondition: boolean)
Returns successfully if someCondition
is true, throws an error if it is not.
assertEquals(actual: {}, expected: {})
assertEquals(actual: {}, expected: {})
Returns successfully if actual
is equal to expected
, throws an error otherwise.
await assertQueryCalled(queryId: string)
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)
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})
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)
await eventTriggers.event(componentId: string)
Triggers event
on the component with the given componentId
(example: await eventTriggers.click('button1')
).
await mock(queryId: string, returnObject: {})
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
Updated 5 months ago