Skip to main content
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:
npm test
This command runs:
  1. Pregenerate - Generates required build files
  2. Linting - ESLint code quality checks
  3. Type checking - TypeScript type validation
  4. Jest tests - All unit and component tests

Individual Test Commands

Run specific test types independently:
npm run jest

Watch Mode

During development, use watch mode for instant feedback:
npm run jest:watch
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

.jest/jest.config.js
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:
.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

index.test.tsx
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:
  1. getByRole - Most accessible, preferred
screen.getByRole('button', { name: /submit/i })
  1. getByLabelText - For form fields
screen.getByLabelText(/username/i)
  1. getByPlaceholderText - Form inputs
screen.getByPlaceholderText(/enter email/i)
  1. getByText - Non-interactive content
screen.getByText(/welcome/i)
  1. 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'))
})
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

1

Start Storybook Server

npm run storybook
Storybook will be available at http://localhost:6006.
2

Build Static Storybook

For deployment or sharing:
npm run storybook:build
Output is generated in storybook-static/ directory.

Writing Stories

Create stories for each component variant:
index.stories.tsx
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:
.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

useCustomHook.test.ts
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

  1. Dependency installation
  2. Pregenerate - Build required files
  3. Linting - Code quality checks
  4. Type checking - TypeScript validation
  5. Jest tests - Full test suite
  6. Coverage upload - Send to CodeClimate

Local CI Simulation

Run the same checks locally before pushing:
npm test
This ensures your code will pass CI checks.

Troubleshooting

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:
jest.config.js
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

Build docs developers (and LLMs) love