UTA DevHub
UI Development/UI Architecture/Foundation Components Guide

Testing & Performance

Testing strategies, visual regression, performance optimization, and memoization patterns for reliable foundation components.

Testing & Performance

Overview

Robust testing and performance optimization are critical for building reliable, fast foundation components. This guide covers comprehensive testing strategies and performance patterns that ensure your components meet quality standards and provide excellent user experiences.

Quality and Speed Matter

Well-tested and performant components:

  • Reduce bugs through comprehensive test coverage
  • Improve user experience with fast, responsive interfaces
  • Enable confident refactoring with regression protection
  • Lower maintenance costs through early issue detection
  • Scale better as your application grows

Testing Strategies

Component Testing Structure

Foundation components require comprehensive testing to ensure reliability and accessibility. Here's a complete testing structure:

ui/foundation/Button/
├── Button.tsx              # Component implementation
├── Button.test.tsx         # Unit tests
├── Button.visual.test.tsx  # Visual regression tests
├── Button.perf.test.tsx    # Performance tests
└── __snapshots__/          # Snapshot files

Test File Organization

// Button.test.tsx structure
describe('Button Component', () => {
  // Rendering tests
  describe('Rendering', () => {
    it('renders with default props', () => {});
    it('renders all variants correctly', () => {});
  });
  
  // Interaction tests
  describe('Interactions', () => {
    it('handles press events', () => {});
    it('prevents interaction when disabled', () => {});
  });
  
  // State tests
  describe('States', () => {
    it('shows loading state', () => {});
    it('communicates disabled state', () => {});
  });
  
  // Accessibility tests
  describe('Accessibility', () => {
    it('has proper role and labels', () => {});
    it('announces state changes', () => {});
  });
});

Unit Testing Patterns

import { render } from '@testing-library/react-native';
import { Button } from './Button';
import { theme } from '@/core/shared/styles/theme';
 
describe('Button Rendering', () => {
  it('renders with required props', () => {
    const { getByText } = render(
      <Button onPress={jest.fn()}>Click me</Button>
    );
    
    expect(getByText('Click me')).toBeTruthy();
  });
  
  it('applies correct variant styles', () => {
    const variants = ['primary', 'secondary', 'ghost', 'danger'] as const;
    
    variants.forEach(variant => {
      const { getByTestId, unmount } = render(
        <Button variant={variant} testID={`button-${variant}`}>
          Test
        </Button>
      );
      
      const button = getByTestId(`button-${variant}`);
      expect(button.props.style).toMatchObject({
        backgroundColor: theme.colors[variant][500],
      });
      
      unmount();
    });
  });
  
  it('renders with icons', () => {
    const Icon = () => <Text>Icon</Text>;
    const { getByText } = render(
      <Button icon={<Icon />} iconRight={<Icon />}>
        Content
      </Button>
    );
    
    expect(getByText('Content')).toBeTruthy();
    expect(getAllByText('Icon')).toHaveLength(2);
  });
  
  it('applies size variations correctly', () => {
    const sizes = {
      small: theme.spacing.sm,
      medium: theme.spacing.md,
      large: theme.spacing.lg,
    };
    
    Object.entries(sizes).forEach(([size, padding]) => {
      const { getByTestId } = render(
        <Button size={size} testID={`button-${size}`}>
          Test
        </Button>
      );
      
      expect(getByTestId(`button-${size}`).props.style).toMatchObject({
        padding,
      });
    });
  });
});

Accessibility Testing

describe('Button Accessibility', () => {
  it('has correct accessibility role', () => {
    const { getByRole } = render(<Button>Test</Button>);
    expect(getByRole('button')).toBeTruthy();
  });
 
  it('communicates disabled state', () => {
    const { getByRole } = render(<Button disabled>Test</Button>);
    const button = getByRole('button');
    expect(button.props.accessibilityState.disabled).toBe(true);
  });
 
  it('communicates loading state', () => {
    const { getByRole } = render(<Button loading>Test</Button>);
    const button = getByRole('button');
    expect(button.props.accessibilityState.busy).toBe(true);
  });
 
  it('has accessible label', () => {
    const { getByLabelText } = render(
      <Button accessibilityLabel="Save changes">Save</Button>
    );
    expect(getByLabelText('Save changes')).toBeTruthy();
  });
  
  it('announces press action', () => {
    const announceForAccessibility = jest.spyOn(
      AccessibilityInfo,
      'announceForAccessibility'
    );
    
    const { getByRole } = render(
      <Button onPress={() => announceForAccessibility('Saved!')}>
        Save
      </Button>
    );
    
    fireEvent.press(getByRole('button'));
    expect(announceForAccessibility).toHaveBeenCalledWith('Saved!');
  });
});

Visual Regression Testing

// Button.visual.test.tsx
import React from 'react';
import renderer from 'react-test-renderer';
import { Button } from './Button';
 
describe('Button Visual Tests', () => {
  it('matches snapshot for all variants', () => {
    const variants = ['primary', 'secondary', 'ghost', 'danger'] as const;
    
    variants.forEach(variant => {
      const tree = renderer.create(
        <Button variant={variant}>Test Button</Button>
      ).toJSON();
      
      expect(tree).toMatchSnapshot(`button-${variant}`);
    });
  });
  
  it('matches snapshot for all sizes', () => {
    const sizes = ['small', 'medium', 'large'] as const;
    
    sizes.forEach(size => {
      const tree = renderer.create(
        <Button size={size}>Test Button</Button>
      ).toJSON();
      
      expect(tree).toMatchSnapshot(`button-${size}`);
    });
  });
  
  it('matches snapshot for different states', () => {
    const states = [
      { loading: true },
      { disabled: true },
      { fullWidth: true },
    ];
    
    states.forEach((state, index) => {
      const tree = renderer.create(
        <Button {...state}>Test Button</Button>
      ).toJSON();
      
      expect(tree).toMatchSnapshot(`button-state-${index}`);
    });
  });
});

Performance Optimization

Memoization Patterns

import React from 'react';
 
// Basic memoization
export const Button = React.memo<ButtonProps>(({ 
  variant = 'primary',
  size = 'medium',
  loading = false,
  disabled = false,
  children,
  onPress,
  ...props 
}) => {
  // Component implementation
});
 
// Custom comparison function
export const Button = React.memo<ButtonProps>(
  (props) => {
    // Component implementation
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return (
      prevProps.variant === nextProps.variant &&
      prevProps.size === nextProps.size &&
      prevProps.loading === nextProps.loading &&
      prevProps.disabled === nextProps.disabled &&
      prevProps.children === nextProps.children
      // Note: Exclude onPress as it might be recreated
    );
  }
);
 
// Memoizing compound components
const ButtonIcon = React.memo(({ icon, position }) => (
  <View style={position === 'left' ? styles.iconLeft : styles.iconRight}>
    {icon}
  </View>
));
 
const ButtonContent = React.memo(({ children, loading }) => {
  if (loading) return <ActivityIndicator />;
  return <Text>{children}</Text>;
});

StyleSheet Optimization

// Static styles - created once
const staticStyles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    textAlign: 'center',
  },
  icon: {
    marginHorizontal: theme.spacing.xs,
  },
});
 
// Dynamic styles - use factory pattern
const createButtonStyles = memoize((variant: string, size: string) => 
  StyleSheet.create({
    button: {
      backgroundColor: theme.colors[variant][500],
      padding: theme.spacing[size],
      borderRadius: theme.radii[size],
    },
  })
);
 
// Flatten styles for performance
const Button = ({ variant, size, style, ...props }) => {
  const dynamicStyles = createButtonStyles(variant, size);
  
  // Flatten only what's needed
  const flattenedStyle = useMemo(
    () => StyleSheet.flatten([
      staticStyles.container,
      dynamicStyles.button,
      style,
    ]),
    [dynamicStyles, style]
  );
  
  return <TouchableOpacity style={flattenedStyle} {...props} />;
};

List Performance

Use FlatList Optimizations

const OptimizedList = ({ data }) => {
  const renderItem = useCallback(({ item }) => (
    <MemoizedListItem item={item} />
  ), []);
  
  const keyExtractor = useCallback((item) => item.id, []);
  
  const getItemLayout = useCallback((data, index) => ({
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT * index,
    index,
  }), []);
  
  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}
      maxToRenderPerBatch={10}
      updateCellsBatchingPeriod={100}
      windowSize={10}
      removeClippedSubviews={true}
      initialNumToRender={10}
    />
  );
};

Implement Virtualization

import { VirtualizedList } from 'react-native';
 
const VirtualizedComponent = ({ items }) => {
  const getItem = useCallback((data, index) => data[index], []);
  const getItemCount = useCallback((data) => data.length, []);
  
  return (
    <VirtualizedList
      data={items}
      getItem={getItem}
      getItemCount={getItemCount}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      // Virtualization config
      maxToRenderPerBatch={5}
      updateCellsBatchingPeriod={50}
      initialNumToRender={10}
      windowSize={21}
    />
  );
};

Use FlashList for Better Performance

import { FlashList } from '@shopify/flash-list';
 
const PerformantList = ({ data }) => {
  return (
    <FlashList
      data={data}
      renderItem={renderItem}
      estimatedItemSize={100}
      // Performance optimizations
      drawDistance={200}
      recycleItems={true}
      overrideItemLayout={(layout, item) => {
        if (item.type === 'header') {
          layout.size = 60;
          layout.span = 2;
        }
      }}
    />
  );
};

Animation Performance

// Use native driver for better performance
const fadeIn = useCallback(() => {
  Animated.timing(opacity, {
    toValue: 1,
    duration: 300,
    useNativeDriver: true, // Critical for performance
  }).start();
}, [opacity]);
 
// Use Reanimated for complex animations
const AnimatedComponent = () => {
  const translateY = useSharedValue(0);
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ translateY: translateY.value }],
  }));
  
  const startAnimation = useCallback(() => {
    'worklet';
    translateY.value = withSpring(100, {
      damping: 15,
      stiffness: 100,
    });
  }, []);
  
  return (
    <Animated.View style={animatedStyle}>
      {/* Content */}
    </Animated.View>
  );
};
 
// Batch animations
const batchedAnimation = useCallback(() => {
  Animated.parallel([
    Animated.timing(opacity, { toValue: 1, useNativeDriver: true }),
    Animated.timing(scale, { toValue: 1, useNativeDriver: true }),
  ]).start();
}, [opacity, scale]);

Performance Testing

// Button.perf.test.tsx
import { measurePerformance } from '@testing-library/react-native-performance';
 
describe('Button Performance', () => {
  it('renders within performance budget', async () => {
    const { renderTime } = await measurePerformance(
      <Button>Test Button</Button>
    );
    
    expect(renderTime).toBeLessThan(16); // 60fps threshold
  });
  
  it('re-renders efficiently', async () => {
    const scenario = async (screen) => {
      const button = screen.getByRole('button');
      
      await screen.rerender(
        <Button variant="secondary">Test Button</Button>
      );
      
      await screen.rerender(
        <Button variant="danger">Test Button</Button>
      );
    };
    
    const { renderCount, renderTime } = await measurePerformance(
      <Button>Test Button</Button>,
      { scenario }
    );
    
    expect(renderCount).toBe(3); // Initial + 2 re-renders
    expect(renderTime.median).toBeLessThan(8);
  });
  
  it('handles rapid interactions efficiently', async () => {
    const onPress = jest.fn();
    const scenario = async (screen) => {
      const button = screen.getByRole('button');
      
      // Simulate rapid taps
      for (let i = 0; i < 10; i++) {
        fireEvent.press(button);
        await wait(50);
      }
    };
    
    const { renderTime } = await measurePerformance(
      <Button onPress={onPress}>Tap me</Button>,
      { scenario }
    );
    
    expect(renderTime.max).toBeLessThan(16);
  });
});

Testing Best Practices

Testing Excellence

Great tests are:

  • Fast: Run quickly to encourage frequent execution
  • Reliable: No flaky tests that fail intermittently
  • Maintainable: Easy to understand and update
  • Comprehensive: Cover all critical paths
  • Isolated: Test one thing at a time

(Do ✅) Testing best practices

  • Write tests before or alongside implementation
  • Test behavior, not implementation details
  • Use descriptive test names that explain the scenario
  • Mock external dependencies consistently
  • Test edge cases and error conditions
  • Keep tests DRY with shared utilities
  • Run tests in CI/CD pipeline
  • Monitor and maintain test coverage
  • Test accessibility features
  • Profile performance regularly

Summary

Comprehensive testing and performance optimization ensure your foundation components are reliable, fast, and maintainable. Key takeaways:

  • Test everything: Aim for high coverage while focusing on critical paths
  • Optimize intelligently: Measure before optimizing
  • Automate quality: Use CI/CD to enforce standards
  • Monitor continuously: Track performance metrics over time
  • Learn from failures: Each bug is an opportunity to improve tests

Next Steps