Documentation Index Fetch the complete documentation index at: https://mintlify.com/metabase/metabase/llms.txt
Use this file to discover all available pages before exploring further.
Metabase has comprehensive test coverage across frontend and backend. Understanding our testing practices will help you write effective tests and catch bugs before they reach production.
Testing philosophy
All code must be tested. Unit tests should be preferred over end-to-end tests as they’re faster to run and debug.
Test types
Unit tests - Test individual functions and components in isolation
Integration tests - Test how multiple units work together
End-to-end tests - Test complete user workflows
Visual regression tests - Catch unintended UI changes
Frontend testing
Running frontend tests
All tests
Unit tests only
Watch mode
With ClojureScript rebuild
Timezone tests
Unit test setup
Frontend unit tests use Jest and React Testing Library. New tests should be placed alongside the components they test.
File naming convention
Unit tests use the .unit.spec.tsx or .unit.spec.ts extension:
Button.tsx
Button.unit.spec.tsx ✅
Standard test pattern
Use this pattern for setting up component tests:
import { render , screen , waitFor } from "@testing-library/react" ;
import userEvent from "@testing-library/user-event" ;
import { renderWithProviders } from "__support__/ui" ;
import { Collection } from "metabase-types/api" ;
import { createMockCollection } from "metabase-types/api/mocks" ;
import CollectionHeader from "./CollectionHeader" ;
interface SetupOpts {
collection : Collection ;
}
const setup = ({ collection } : SetupOpts ) => {
const onUpdateCollection = jest . fn ();
renderWithProviders (
< CollectionHeader
collection = { collection }
onUpdateCollection = { onUpdateCollection }
/> ,
);
return { onUpdateCollection };
};
describe ( "CollectionHeader" , () => {
it ( "should update collection name" , async () => {
const collection = createMockCollection ({
name: "Old name" ,
});
const { onUpdateCollection } = setup ({ collection });
await userEvent . clear ( screen . getByDisplayValue ( "Old name" ));
await userEvent . type (
screen . getByPlaceholderText ( "Add title" ),
"New name"
);
await userEvent . tab ();
expect ( onUpdateCollection ). toHaveBeenCalledWith ({
... collection ,
name: "New name" ,
});
});
});
Key testing utilities
Renders components with all necessary providers (Redux, Router, Theme): import { renderWithProviders } from "__support__/ui" ;
renderWithProviders (< MyComponent />);
Create mock data for tests: import {
createMockCollection ,
createMockDatabase ,
createMockCard ,
} from "metabase-types/api/mocks" ;
const collection = createMockCollection ({ name: "Test" });
Mock API responses using fetch-mock: import { setupCollectionsEndpoints } from "__support__/server-mocks" ;
const setup = () => {
setupCollectionsEndpoints ({ collections: [ collection ] });
renderWithProviders (< Component />);
};
API request mocking
Use fetch-mock to mock HTTP requests:
import fetchMock from "fetch-mock" ;
import { setupDatabasesEndpoints } from "__support__/server-mocks" ;
const setup = () => {
const databases = [
createMockDatabase ({ id: 1 , name: "Test DB" }),
];
setupDatabasesEndpoints ({ databases });
renderWithProviders (< DatabaseList />);
};
describe ( "DatabaseList" , () => {
beforeEach (() => {
fetchMock . reset ();
});
it ( "displays databases" , async () => {
setup ();
expect ( await screen . findByText ( "Test DB" )). toBeInTheDocument ();
});
});
Testing best practices
Do not use fetch-mock outside of the setup function. Always use helper functions from __support__/server-mocks.
Test behavior, not implementation - Focus on what users see and do
Use semantic queries - Prefer getByRole, getByLabelText over getByTestId
Wait for async updates - Use findBy* queries or waitFor for async operations
Keep tests isolated - Each test should be independent
Mock external dependencies - Don’t make real API calls
Backend testing
Running backend tests
OSS tests
OSS + Enterprise
Specific namespace
Specific test
Tests in directory
Writing backend tests
Backend tests use clojure.test and are located in /test/metabase/:
( ns metabase.api.database-test
( :require
[clojure.test :refer :all ]
[metabase.test :as mt]))
( deftest list-databases-test
( testing "GET /api/database"
( mt/with-temp [ :model/Database {db-id :id } {}]
( is ( = [{ :id db-id}]
( mt/user-http-request :crowberto :get 200 "database" ))))))
Test utilities
Create temporary database records for testing: ( mt/with-temp [ :model/Database {db-id :id } { :name "Test DB" }
:model/Table {table-id :id } { :db_id db-id}]
;; Test code here
)
Make authenticated API requests: ( mt/user-http-request :crowberto :get 200 "database" )
( mt/user-http-request :rasta :post 403 "database" { :name "Test" })
Use specific test datasets: ( mt/dataset test-data
( mt/run-mbql-query venues
{ :aggregation [ :count ]}))
Testing with multiple drivers
By default, tests run against H2. To test other drivers:
DRIVERS = h2,postgres,mysql,mongo clojure -X:dev:drivers:drivers-dev:test
In the REPL:
( mt/set-test-drivers! #{ :postgres :mysql :h2 })
Driver-specific test data
Each driver can provide test data in metabase.test.data.<driver>:
( ns metabase.test.data.postgres
( :require [metabase.test.data.interface :as tx]))
( defmethod tx / dbdef->connection-details :postgres
[_ context { :keys [database-name]}]
{ :host ( tx/db-test-env-var-or-throw :postgresql :host "localhost" )
:port ( tx/db-test-env-var-or-throw :postgresql :port 5432 )
:user ( tx/db-test-env-var :postgresql :user )
:password ( tx/db-test-env-var :postgresql :password )
:db ( when ( = context :db ) database-name)})
REPL-driven testing
Run tests from the REPL for faster feedback:
;; Run a single test
( clojure.test/run-test-var #'metabase.api.database-test/list-databases-test)
;; Run all tests in namespace
( clojure.test/run-tests 'metabase.api.database-test)
;; Run tests matching pattern
( metabase.test-runner/find-and-run-tests-repl
{ :namespace-pattern ".*database.*" })
End-to-end testing
E2E tests use Cypress and simulate real user workflows.
Running Cypress tests
All e2e tests
Open Cypress UI
Specific spec
Writing Cypress tests
Cypress tests use the .cy.spec.js extension:
import { restore , popover } from "e2e/support/helpers" ;
describe ( "scenarios > question > new" , () => {
beforeEach (() => {
restore ();
cy . signInAsAdmin ();
});
it ( "should create a new question" , () => {
cy . visit ( "/" );
cy . findByText ( "New" ). click ();
popover (). within (() => {
cy . findByText ( "Question" ). click ();
});
cy . findByText ( "Sample Database" ). click ();
cy . findByText ( "Orders" ). click ();
cy . findByTestId ( "qb-header" ). findByText ( "Visualize" ). click ();
cy . findByText ( "18,760" );
});
});
Cypress best practices
Use custom commands - Defined in /e2e/support/commands.js
Wait for elements - Use cy.findBy* which automatically waits
Use helpers - Import from /e2e/support/helpers
Reset state - Use restore() before each test
Avoid hardcoded waits - Don’t use cy.wait(1000)
Visual regression testing
Metabase uses Loki for visual regression tests:
# Run visual tests
bun run test-visual:loki
# Approve differences
bun run test-visual:loki-approve-diff
# Generate report
bun run test-visual:loki-report
Visual tests run against Storybook stories.
Test data and fixtures
Frontend test data
Create mock data using factory functions:
import { createMockCard } from "metabase-types/api/mocks" ;
const card = createMockCard ({
name: "Test Question" ,
dataset_query: {
type: "query" ,
database: 1 ,
},
});
Backend test data
Use mt/with-temp or test datasets:
;; Temporary data
( mt/with-temp [ :model/Card {card-id :id } { :name "Test" }]
;; Test code
)
;; Test datasets
( mt/dataset test-data
( mt/id :venues )) ; => table ID
Continuous integration
All tests run in CI on every pull request:
Frontend unit tests
Backend unit tests
Cypress e2e tests
Visual regression tests
Linters
PRs must pass all tests before merging. Fix failing tests or explain why they’re expected to fail.
Debugging tests
Frontend debugging
# Run with Node debugger
bun run test-debug
# Run specific test file
bun run test-unit frontend/src/metabase/components/Button.unit.spec.tsx
Backend debugging
From the REPL:
;; Set breakpoint
( require '[clojure.tools.trace :as trace])
( trace/trace-ns 'metabase.api.database)
;; Run test
( clojure.test/run-test-var #'metabase.api.database-test/list-databases-test)
Cypress debugging
Open Cypress UI for interactive debugging:
Use browser DevTools and time-travel debugging.
Next steps