Testing
This guide covers testing practices for both frontend and backend code in Grafana.
Frontend testing
Recommended practices
- Default to
*ByRole queries - They encourage testing with accessibility in mind
- Alternative:
*ByLabelText queries - Though *ByRole queries are more robust
- Use React Testing Library - Don’t create new snapshot tests; we’re removing existing ones
- Migrate Enzyme tests - When modifying existing tests, migrate from Enzyme to RTL (unless fixing a bug)
Testing user interactions
Use the user-event library instead of fireEvent:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function setup(jsx: JSX.Element) {
return {
user: userEvent.setup(),
...render(jsx),
};
}
it('should render', async () => {
const { user } = setup(<Button />);
await user.click(screen.getByRole('button'));
});
- All
userEvent methods are asynchronous - use await
- Call
userEvent.setup() before tests (use the utility function pattern above)
Debugging tests
Useful utilities:
screen.debug() - Prints DOM tree to console
- Testing Playground - Interactive sandbox for testing queries
prettyDOM logRoles - Prints implicit ARIA roles for a DOM tree
Testing Select components
Query by label
Add a label element with htmlFor matching the inputId, or use aria-label:
it('should render the picker', () => {
render(
<>
<label htmlFor={'role-picker'}>Role picker</label>
<OrgRolePicker value={OrgRole.Admin} inputId={'role-picker'} onChange={() => {}} />
</>
);
expect(screen.getByRole('combobox', { name: 'Role picker' })).toBeInTheDocument();
});
Test displayed options
Click the Select and match options using *ByText:
it('should have an "Editor" option', async () => {
const { user } = setup(
<>
<label htmlFor={'role-picker'}>Role picker</label>
<OrgRolePicker value={OrgRole.Admin} inputId={'role-picker'} onChange={() => {}} />
</>
);
await user.click(screen.getByRole('combobox', { name: 'Role picker' }));
expect(screen.getByText('Editor')).toBeInTheDocument();
});
Select an option
Use the selectOptionInTest utility (wraps react-select-event):
it('should select an option', async () => {
const mockOnChange = jest.fn();
setup(
<>
<label htmlFor={'role-picker'}>Role picker</label>
<OrgRolePicker value={OrgRole.Admin} inputId={'role-picker'} onChange={mockOnChange} />
</>
);
await selectOptionInTest(screen.getByRole('combobox', { name: 'Role picker' }), 'Viewer');
expect(mockOnChange).toHaveBeenCalledWith('Viewer');
});
Mocking
Mock window object
Use Jest spies (they auto-restore):
let windowSpy: jest.SpyInstance;
beforeAll(() => {
windowSpy = jest.spyOn(window, 'location', 'get');
});
afterAll(() => {
windowSpy.mockRestore();
});
it('should test with window', () => {
windowSpy.mockImplementation(() => ({
href: 'www.example.com',
}));
expect(window.location.href).toBe('www.example.com');
});
Mock getBackendSrv()
For HTTP requests to Grafana backend:
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
post: postMock,
}),
}));
Mock getBackendSrv for AsyncSelect
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getBackendSrv: () => ({
get: () =>
Promise.resolve([
{ name: 'Org 1', id: 0 },
{ name: 'Org 2', id: 1 },
]),
}),
}));
it('should have the options', async () => {
const { user } = setup(
<>
<label htmlFor={'picker'}>Org picker</label>
<OrgPicker onSelected={() => {}} inputId={'picker'} />
</>
);
await user.click(await screen.findByRole('combobox', { name: 'Org picker' }));
expect(screen.getByText('Org 1')).toBeInTheDocument();
expect(screen.getByText('Org 2')).toBeInTheDocument();
});
Running frontend tests
# Run specific test file
yarn test path/to/file
# Run by name pattern
yarn test -t "pattern"
# Update snapshots
yarn test -u
# Run once (no watch mode)
yarn jest --no-watch
# Coverage report
yarn test:coverage
# CI mode
yarn test:ci
Backend testing
Test framework
Use the standard library testing package. For assertions, prefer testify.
TestMain and test suite
Each package SHOULD include a TestMain function that calls testsuite.Run(m):
package mypkg
import (
"testing"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
For tests that use the database, you MUST define TestMain so test databases can be cleaned up properly.
Only define TestMain once per package (in one _test.go file).
Integration tests
Mark integration tests properly to keep CI running smoothly:
func TestIntegrationFoo(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
// test body
}
If you don’t follow this convention, your integration test may run twice or not at all.
Assertions
- Use
assert.* for “soft checks” (don’t halt test)
- Use
require.* for “hard checks” (halt test on failure)
Typically use require.* for error assertions, since continuing after an error is chaotic and often causes segfaults.
Sub-tests
Use t.Run to group test cases:
func TestFeature(t *testing.T) {
t.Run("case 1", func(t *testing.T) {
// test case 1
})
t.Run("case 2", func(t *testing.T) {
// test case 2
})
}
This allows:
- Common setup/teardown code
- Running test cases in isolation when debugging
Don’t use t.Run just to group assertions.
Cleanup
Use t.Cleanup instead of defer (it works in helper functions):
func TestFeature(t *testing.T) {
resource := setup()
t.Cleanup(func() {
resource.Close()
})
// test body
}
Cleanup functions execute in reverse order after the test completes.
Mocking with testify/mock
Basic mock implementation
Given this interface:
type MyInterface interface {
Get(ctx context.Context, id string) (Object, error)
}
Mock it like this:
import "github.com/stretchr/testify/mock"
type MockImplementation struct {
mock.Mock
}
func (m *MockImplementation) Get(ctx context.Context, id string) (Object, error) {
args := m.Called(ctx, id)
return args.Get(0).(Object), args.Error(1)
}
Using mocks in tests
objectToReturn := Object{Message: "abc"}
errToReturn := errors.New("my error")
myMock := &MockImplementation{}
defer myMock.AssertExpectations(t)
myMock.On("Get", mock.Anything, "id1").Return(Object{}, errToReturn).Once()
myMock.On("Get", mock.Anything, "id2").Return(objectToReturn, nil).Once()
anyService := NewService(myMock)
resp, err := anyService.Call("id1")
assert.Error(t, err, errToReturn)
resp, err = anyService.Call("id2")
assert.Nil(t, err)
assert.Equal(t, resp.Message, objectToReturn.Message)
Tips:
- Use
Once() or Times(n) to limit call count
- Use
mockedClass.AssertExpectations(t) to verify call counts
- Use
mock.Anything when you don’t care about the argument
- Use
mockedClass.AssertNotCalled(t, "MethodName") to assert a method wasn’t called
Generate mocks with mockery
For large interfaces, use mockery to generate mocks:
mockery --name InterfaceName --structname MockImplementationName --inpackage --filename my_implementation_mock.go
Or add a go:generate comment:
package mypackage
//go:generate mockery --name InterfaceName --structname MockImplementationName --inpackage --filename my_implementation_mock.go
Running backend tests
# Specific test
go test -run TestName ./pkg/services/myservice/
# All unit tests
make test-go-unit
# Integration tests
make test-go-integration
# With database backends
make devenv sources=postgres_tests,mysql_tests
make test-go-integration-postgres
Test requirements for pull requests
- Include tests that verify the intended behavior
- For bug fixes, include tests that replicate the bug (prevent regressions)
- Run appropriate tests as part of your PR
- Ensure all CI checks pass before merge