The AgrospAI Data Space Portal uses a comprehensive testing approach combining unit tests, component tests, and visual testing through Storybook.
Testing Stack
The project uses the following testing tools:
Storybook stories automatically generate tests through the StoryShots addon, providing good coverage with minimal effort.
Running Tests
Full Test Suite
Run the complete test suite including linting, type checking, and tests:
This command runs:
Pregenerate - Generates required build files
Linting - ESLint code quality checks
Type checking - TypeScript type validation
Jest tests - All unit and component tests
Individual Test Commands
Run specific test types independently:
Jest Only
Linting
Type Check
Format Code
Watch Mode
During development, use watch mode for instant feedback:
This runs tests continuously and shows:
Real-time test results
Coverage report updates
File change detection
Watch mode only reruns tests affected by your changes, making it very efficient for development.
Jest Configuration
Jest is configured in .jest/jest.config.js with Next.js integration.
Configuration Overview
const nextJest = require ( 'next/jest' )
const createJestConfig = nextJest ({
dir: './' // Path to Next.js app
})
const customJestConfig = {
rootDir: '../' ,
setupFilesAfterEnv: [ '<rootDir>/.jest/jest.setup.tsx' ],
testEnvironment: 'jsdom' ,
moduleDirectories: [ 'node_modules' , '<rootDir>/src' ],
// Path aliases matching tsconfig.json
moduleNameMapper: {
'^.+ \\ .(svg)$' : '<rootDir>/.jest/__mocks__/svgrMock.tsx' ,
'@components/(.*)$' : '<rootDir>/src/components/$1' ,
'@shared(.*)$' : '<rootDir>/src/components/@shared/$1' ,
'@hooks/(.*)$' : '<rootDir>/src/@hooks/$1' ,
'@context/(.*)$' : '<rootDir>/src/@context/$1' ,
'@images/(.*)$' : '<rootDir>/src/@images/$1' ,
'@utils/(.*)$' : '<rootDir>/src/@utils/$1' ,
'@content/(.*)$' : '<rootDir>/@content/$1'
},
// Coverage configuration
collectCoverage: true ,
collectCoverageFrom: [
'src/**/*.{ts,tsx}' ,
'!src/**/*.{stories,test}.{ts,tsx}' ,
'!src/@types/**/*.{ts,tsx}'
],
// Ignore patterns
testPathIgnorePatterns: [
'<rootDir>/node_modules/' ,
'<rootDir>/.next/' ,
'<rootDir>/coverage'
],
// ESM package handling
transformIgnorePatterns: [ '/node_modules/(?!uuid|remark)/' ]
}
Test Setup
The test environment is configured in .jest/jest.setup.tsx:
import '@testing-library/jest-dom/extend-expect'
import { jest } from '@jest/globals'
import './__mocks__/matchMedia'
import './__mocks__/hooksMocks'
import './__mocks__/connectkit'
// Mock Next.js router
jest . mock ( 'next/router' , () => ({
useRouter: jest . fn (). mockImplementation (() => ({
route: '/' ,
pathname: '/'
}))
}))
Writing Component Tests
Test File Structure
Tests should be colocated with components:
src
└── components
└── @shared
└── AddToken
├── index.tsx # Component
├── index.module.css # Styles
├── index.stories.tsx # Storybook stories
└── index.test.tsx # Tests
Basic Test Example
import { render , screen , fireEvent } from '@testing-library/react'
import testRender from '../../../../.jest/testRender'
import AddToken from './index'
// Mock dependencies
jest . mock ( '../../../@utils/wallet' , () => ({
addTokenToWallet: jest . fn ()
}))
describe ( '@shared/AddToken' , () => {
const propsBase = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8' ,
symbol: 'OCEAN'
}
// Snapshot test using testRender helper
testRender (< AddToken { ... propsBase } />)
it ( 'renders with custom text' , () => {
render (< AddToken { ... propsBase } text = "Hello Text" />)
expect ( screen . getByText ( 'Hello Text' )). toBeInTheDocument ()
})
it ( 'handles click events' , () => {
render (< AddToken { ... propsBase } />)
fireEvent . click ( screen . getByRole ( 'button' ))
// Add assertions for expected behavior
})
it ( 'renders minimal variant' , () => {
render (< AddToken { ... propsBase } minimal />)
expect ( screen . getByRole ( 'button' )). toHaveClass ( 'minimal' )
})
})
Testing Library Best Practices
Use queries in this order of priority:
getByRole - Most accessible, preferred
screen . getByRole ( 'button' , { name: /submit/ i })
getByLabelText - For form fields
screen . getByLabelText ( /username/ i )
getByPlaceholderText - Form inputs
screen . getByPlaceholderText ( /enter email/ i )
getByText - Non-interactive content
screen . getByText ( /welcome/ i )
getByTestId - Last resort only
screen . getByTestId ( 'custom-element' )
Use waitFor and findBy queries for async behavior: import { waitFor } from '@testing-library/react'
it ( 'loads data asynchronously' , async () => {
render (< DataComponent />)
// Wait for element to appear
const data = await screen . findByText ( /loaded data/ i )
expect ( data ). toBeInTheDocument ()
// Or wait for condition
await waitFor (() => {
expect ( screen . getByText ( /complete/ i )). toBeInTheDocument ()
})
})
Use fireEvent or userEvent for interactions: import { fireEvent } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
it ( 'handles user input' , async () => {
render (< Form />)
// fireEvent - synchronous
const input = screen . getByLabelText ( /name/ i )
fireEvent . change ( input , { target: { value: 'John' } })
// userEvent - more realistic, async
const user = userEvent . setup ()
await user . type ( input , 'John Doe' )
await user . click ( screen . getByRole ( 'button' ))
})
Mocking Hooks and Context
Mock custom hooks and React context: // Mock custom hook
jest . mock ( '@hooks/useAsset' , () => ({
useAsset : () => ({
ddo: mockDDO ,
isInPurgatory: false ,
loading: false
})
}))
// Test with context
import { AssetProvider } from '@context/Asset'
const wrapper = ({ children }) => (
< AssetProvider value = { mockContextValue } >
{ children }
</ AssetProvider >
)
render (< Component />, { wrapper })
Storybook
Storybook provides an isolated environment for developing and testing UI components.
Running Storybook
Start Storybook Server
Storybook will be available at http://localhost:6006.
Build Static Storybook
For deployment or sharing: Output is generated in storybook-static/ directory.
Writing Stories
Create stories for each component variant:
import { ComponentStory , ComponentMeta } from '@storybook/react'
import AddToken , { AddTokenProps } from '@shared/AddToken'
export default {
title: 'Component/@shared/AddToken' ,
component: AddToken
} as ComponentMeta < typeof AddToken >
const Template : ComponentStory < typeof AddToken > = ( args : AddTokenProps ) => {
return < AddToken { ... args } />
}
interface Props {
args: AddTokenProps
}
export const Default: Props = Template.bind({})
Default . args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8' ,
symbol: 'OCEAN'
}
export const Minimal : Props = Template . bind ({})
Minimal . args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8' ,
symbol: 'OCEAN' ,
minimal: true
}
export const WithCustomText : Props = Template . bind ({})
WithCustomText . args = {
address: '0xd8992Ed72C445c35Cb4A2be468568Ed1079357c8' ,
symbol: 'OCEAN' ,
text: 'Add Custom Token'
}
Storybook Configuration
Storybook is configured in .storybook/main.js:
const TsconfigPathsPlugin = require ( 'tsconfig-paths-webpack-plugin' )
module . exports = {
core: { builder: 'webpack5' },
stories: [ '../src/**/*.stories.tsx' ],
addons: [ '@storybook/addon-essentials' ],
framework: '@storybook/react' ,
webpackFinal : async ( config ) => {
// Add TypeScript path aliases
config . resolve . plugins = [
... ( config . resolve . plugins || []),
new TsconfigPathsPlugin ({
extensions: config . resolve . extensions
})
]
// SVG handling
const fileLoaderRule = config . module . rules . find (
( rule ) => rule . test && rule . test . test ( '.svg' )
)
fileLoaderRule . exclude = / \. svg $ /
config . module . rules . push ({
test: / \. svg $ / ,
issuer: / \. ( tsx | ts ) $ / ,
use: [{ loader: '@svgr/webpack' , options: { icon: true } }]
})
return config
}
}
StoryShots Integration
All Storybook stories automatically generate snapshot tests through the StoryShots addon. No additional configuration needed!
// This happens automatically:
// ✓ Component/@shared/AddToken › Default
// ✓ Component/@shared/AddToken › Minimal
// ✓ Component/@shared/AddToken › WithCustomText
Code Coverage
Coverage reports are automatically generated with every test run.
Viewing Coverage
Coverage summary appears in the console:
-------------------- | --------- | ---------- | --------- | --------- | -------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------- | --------- | ---------- | --------- | --------- | -------------------
All files | 78.45 | 65.23 | 82.11 | 79.34 |
components | 85.67 | 72.45 | 88.92 | 86.23 |
AddToken/index.tsx | 92.31 | 85.71 | 100.0 | 91.67 | 45-48
-------------------- | --------- | ---------- | --------- | --------- | -------------------
HTML Coverage Report
Detailed HTML report is generated in coverage/lcov-report/index.html:
# Open coverage report
open coverage/lcov-report/index.html
Coverage Configuration
Coverage is collected from:
✅ All TypeScript/TSX files in src/
❌ Test files (*.test.ts, *.test.tsx)
❌ Story files (*.stories.tsx)
❌ Type definitions (src/@types)
Coverage reports are sent to CodeClimate during CI runs for tracking over time.
Testing Patterns
Testing Hooks
import { renderHook , act } from '@testing-library/react'
import { useCustomHook } from './useCustomHook'
describe ( 'useCustomHook' , () => {
it ( 'initializes with default values' , () => {
const { result } = renderHook (() => useCustomHook ())
expect ( result . current . value ). toBe ( 0 )
expect ( result . current . loading ). toBe ( false )
})
it ( 'updates value on action' , () => {
const { result } = renderHook (() => useCustomHook ())
act (() => {
result . current . increment ()
})
expect ( result . current . value ). toBe ( 1 )
})
})
Testing with Ocean.js
import { Ocean } from '@oceanprotocol/lib'
jest . mock ( '@oceanprotocol/lib' )
const mockOcean = {
assets: {
resolve: jest . fn (). mockResolvedValue ( mockDDO )
}
}
beforeEach (() => {
;( Ocean as jest . Mock ). mockImplementation (() => mockOcean )
})
it ( 'fetches asset metadata' , async () => {
const { result } = renderHook (() => useAsset ( 'did:op:123' ))
await waitFor (() => {
expect ( result . current . ddo ). toEqual ( mockDDO )
})
})
Testing GraphQL Queries
import { createClient , Provider } from 'urql'
import { never } from 'wonka'
const mockClient = createClient ({
url: 'http://localhost:9000' ,
exchanges: [],
executeQuery : () => never
})
jest . spyOn ( mockClient , 'executeQuery' ). mockReturnValue ({
data: { users: mockUsers },
error: undefined
})
render (
< Provider value = { mockClient } >
< UserList />
</ Provider >
)
Continuous Integration
Tests run automatically on CI for every commit and pull request.
CI Test Pipeline
Dependency installation
Pregenerate - Build required files
Linting - Code quality checks
Type checking - TypeScript validation
Jest tests - Full test suite
Coverage upload - Send to CodeClimate
Local CI Simulation
Run the same checks locally before pushing:
This ensures your code will pass CI checks.
Troubleshooting
Tests failing with module not found
Ensure path aliases in jest.config.js match tsconfig.json: # Clear Jest cache
npm run jest -- --clearCache
# Run tests again
npm run jest
If snapshots are outdated after intentional changes: # Update all snapshots
npm run jest -- -u
# Update specific snapshot
npm run jest -- -u ComponentName
Review changes carefully before committing!
Common fixes: # Clear Storybook cache
rm -rf node_modules/.cache/storybook
# Reinstall dependencies
rm -rf node_modules
npm install
# Restart Storybook
npm run storybook
Ensure coverage is enabled in Jest config: collectCoverage : true ,
collectCoverageFrom : [
'src/**/*.{ts,tsx}' ,
'!src/**/*.{stories,test}.{ts,tsx}'
]
Run Jest directly: npm run jest -- --coverage
Increase test timeout: it ( 'async operation' , async () => {
// Increase timeout to 10s
jest . setTimeout ( 10000 )
await longRunningOperation ()
}, 10000 ) // Or set timeout here
Best Practices
Test Behavior, Not Implementation Focus on what the component does, not how it does it. Test user interactions and expected outcomes.
Use Storybook for Visual Tests Create stories for all component variants. They auto-generate tests and provide documentation.
Keep Tests Fast Mock external dependencies. Use jest.fn() for functions and jest.mock() for modules.
Maintain High Coverage Aim for >80% coverage. Focus on critical paths and edge cases.
Run Tests Before Committing Always run npm test before pushing. Pre-commit hooks help enforce this.
Write Descriptive Test Names Test names should clearly describe what they test. Use it('does something specific when...').
Next Steps
Local Development Set up your development environment
Barge Integration Test with local Ocean components
Components Reference Learn about component testing
Production Build Prepare for production deployment