Skip to content

rasmustoender/AngularTest

Repository files navigation

Angular Ideal Setup

This project demonstrates a potential Angular project structure showcasing best (of my knowledge) practices for scalable applications. It includes a feature-based architecture (instead of component-based), state management, and maintainability strategies.

Table of Contents

Project Overview

This Angular project structure is designed to be:

  • Scalable: Easy to add new features without affecting existing code
  • Maintainable: Clear separation of concerns and consistent patterns
  • Testable: Components and services are easily testable in isolation
  • Performant: Lazy loading, OnPush change detection, and optimized bundles

Architecture Decisions

Feature-Based Architecture

The project follows a feature-based architecture where each feature is self-contained with its own:

  • Module (feature.module.ts)
  • Routing (feature-routing.module.ts)
  • Components
  • Services
  • Models/Interfaces
  • Guards (if feature-specific)

Why?

  • Clear boundaries between features
  • Easy to locate feature-related code
  • Supports lazy loading for better performance
  • Enables team members to work on different features independently

Core vs Shared Modules

Core Module (src/app/core/)

  • Contains singleton services, guards, and interceptors
  • Imported once in AppModule
  • Includes app-wide services like AuthService, ApiService
  • Prevents duplicate service instances

Shared Module (src/app/shared/)

  • Contains reusable components, directives, and pipes
  • Imported by feature modules that need shared functionality
  • Examples: ButtonComponent, LoadingSpinnerComponent
  • Do NOT import in CoreModule or AppModule

Why?

  • Prevents circular dependencies
  • Clear distinction between app-wide and reusable code
  • Better tree-shaking and bundle optimization

Layout Module

Layout Module (src/app/layout/)

  • Contains layout components: HeaderComponent, SidebarComponent, FooterComponent
  • Imported in AppModule
  • Provides consistent application shell

Folder Structure

src/
├── app/
│   ├── core/                    # Singleton services, guards, interceptors
│   │   ├── guards/
│   │   │   └── auth.guard.ts
│   │   ├── interceptors/
│   │   │   ├── auth.interceptor.ts
│   │   │   └── error.interceptor.ts
│   │   ├── services/
│   │   │   ├── api.service.ts
│   │   │   └── auth.service.ts
│   │   ├── models/
│   │   ├── core.module.ts
│   │   └── index.ts             # Barrel export
│   │
│   ├── shared/                  # Shared components, directives, pipes
│   │   ├── components/
│   │   │   ├── button/
│   │   │   └── loading-spinner/
│   │   ├── directives/          # Placeholder for custom directives
│   │   ├── pipes/               # Placeholder for custom pipes
│   │   ├── utils/
│   │   ├── shared.module.ts
│   │   └── index.ts             # Barrel export
│   │
│   ├── features/                # Feature modules (lazy-loaded)
│   │   └── dashboard/
│   │       ├── components/
│   │       ├── services/
│   │       ├── models/
│   │       ├── dashboard.module.ts
│   │       └── dashboard-routing.module.ts
│   │
│   ├── layout/                  # Layout components
│   │   ├── header/
│   │   ├── sidebar/
│   │   ├── footer/
│   │   └── layout.module.ts
│   │
│   ├── store/                   # NgRx store
│   │   ├── state/
│   │   │   ├── app.state.ts
│   │   │   └── dashboard.state.ts
│   │   ├── actions/
│   │   │   └── dashboard.actions.ts
│   │   ├── reducers/
│   │   │   ├── dashboard.reducer.ts
│   │   │   └── index.ts
│   │   ├── effects/
│   │   │   ├── dashboard.effects.ts
│   │   │   └── index.ts
│   │   └── index.ts
│   │
│   ├── app.module.ts
│   ├── app-routing.module.ts
│   └── app.component.*
│
├── assets/                      # Static assets
├── environments/                # Environment configurations
│   ├── environment.ts
│   └── environment.prod.ts
├── styles/                      # Global styles
│   └── styles.scss
└── main.ts                      # Application entry point

Key Files Explained

Barrel Exports (index.ts)

Barrel exports provide clean imports:

// Instead of:
import { AuthGuard } from '@core/guards/auth.guard';
import { ApiService } from '@core/services/api.service';

// Use:
import { AuthGuard, ApiService } from '@core';

Path Aliases (tsconfig.json)

Configured path aliases for cleaner imports:

  • @core/*src/app/core/*
  • @shared/*src/app/shared/*
  • @features/*src/app/features/*
  • @environments/*src/environments/*

State Management

Primary Approach: NgRx

For complex applications with shared state, NgRx is recommended:

Structure:

store/
├── state/          # State interfaces
├── actions/        # Action creators
├── reducers/       # State reducers
└── effects/        # Side effects

Benefits:

  • Predictable state management
  • Time-travel debugging with Redux DevTools
  • Centralized state
  • Easy to test
  • Great for complex state interactions

Example Usage:

// Component
this.store.dispatch(loadDashboardStats());

// Selector
this.stats$ = this.store.select(selectDashboardStats);

Alternative Approach: Services with RxJS

For simpler state or feature-specific state, use Services with BehaviorSubjects:

Example:

@Injectable()
export class FeatureService {
  private stateSubject = new BehaviorSubject<FeatureState>(initialState);
  public state$ = this.stateSubject.asObservable();
}

When to Use:

  • Feature-specific state that doesn't need to be shared
  • Simple state management needs
  • Smaller applications

Design Patterns

Smart/Dumb Component Pattern

Smart Components (Container Components)

  • Located in feature modules
  • Handle data fetching and business logic
  • Connect to services or NgRx store
  • Example: DashboardComponent

Dumb Components (Presentational Components)

  • Located in shared module or feature components
  • Receive data via @Input()
  • Emit events via @Output()
  • No direct service dependencies
  • Example: DashboardStatsComponent, ButtonComponent

OnPush Change Detection

Components use ChangeDetectionStrategy.OnPush for better performance:

  • Only checks for changes when:
    • Input references change
    • Events occur
    • Observables emit (with async pipe)

Dependency Injection

All services use Angular's dependency injection:

  • Services are provided in modules or use providedIn: 'root'
  • Easy to mock for testing
  • Loose coupling between components and services

Technologies & Tools

Core

  • Angular 21: Latest Angular version with modern features
  • TypeScript 5.9: Strict mode enabled for type safety
  • RxJS: Reactive programming for async operations

State Management

  • NgRx 20: Store, Effects (for complex state)
  • RxJS BehaviorSubjects: For simpler state management

Development Tools

  • Angular DevTools: Browser extension for debugging
  • TypeScript: Built-in type checking

Testing

  • Vitest: Fast unit testing with Angular support
  • Playwright: E2E testing across multiple browsers

Getting Started

Prerequisites

  • Node.js (v18 or higher)
  • npm or yarn

Installation

  1. Install dependencies:
npm install
  1. Install Playwright browsers (required for E2E tests):
npx playwright install
  • Downloads browser binaries for Chromium, Firefox, and WebKit
  • Required before running E2E tests (npm run test:e2e)
  • Only needs to be run once after npm install

Available Commands

Development Commands

Start Development Server

npm start
# or
npm start -- --port 4201  # Use different port if 4200 is in use
  • Starts Angular development server on http://localhost:4200
  • Hot module replacement enabled
  • Auto-reloads on file changes

Build Application

npm run build
  • Creates production build in dist/ folder
  • Optimized and minified for production

Build in Watch Mode

npm run watch
  • Builds application and watches for changes
  • Rebuilds automatically when files change
  • Useful for development builds

Testing Commands

Run Unit Tests (Once)

npm test
# or
npm run test
  • Runs all unit tests with Vitest
  • Exits after completion
  • Shows test results summary

Run Unit Tests in Watch Mode

npm run test:watch
  • Runs tests in watch mode
  • Automatically re-runs tests when files change
  • Great for test-driven development

Run Tests with Coverage

npm run test:coverage
  • Runs all tests and generates coverage report
  • Creates coverage report in coverage/ folder
  • Shows statement, branch, function, and line coverage

Open Test UI (Interactive)

npm run test:ui
  • Opens Vitest's interactive UI in browser
  • Visual test exploration and debugging
  • Filter and run specific tests

Run E2E Tests

npm run test:e2e
  • Runs Playwright end-to-end tests
  • Tests across multiple browsers (Chromium, Firefox, WebKit)
  • Headless mode by default
  • Note: Requires browsers to be installed first with npx playwright install

Run E2E Tests with UI

npm run test:e2e:ui
  • Opens Playwright's interactive test UI
  • Visual test execution and debugging
  • Step through tests visually

Run E2E Tests in Headed Mode

npm run test:e2e:headed
  • Runs E2E tests with visible browser
  • Useful for debugging test failures
  • See what the browser is doing during tests

Best Practices

Component Organization

  • One component per file
  • Component files: component-name.component.ts|html|scss
  • Use OnPush change detection when possible
  • Keep components focused and small

Service Organization

  • Core services in core/services/
  • Feature services in features/[feature]/services/
  • Use dependency injection
  • Make services testable (no side effects in constructors)

Module Organization

  • Feature modules are lazy-loaded
  • Core module imported once
  • Shared module imported by features
  • Avoid circular dependencies

Naming Conventions

  • Components: PascalCase (DashboardComponent)
  • Services: PascalCase with "Service" suffix (DashboardService)
  • Interfaces: PascalCase (DashboardStats)
  • Files: kebab-case (dashboard-stats.component.ts)
  • Selectors: kebab-case with prefix (app-dashboard)

Code Style

  • Use TypeScript strict mode
  • Prefer interfaces over types for object shapes
  • Use async/await for promises
  • Use RxJS operators for observables
  • Keep functions small and focused

Scalability Notes

Adding New Features

  1. Create feature folder in src/app/features/
  2. Generate feature module:
    feature-name/
    ├── components/
    ├── services/
    ├── models/
    ├── feature-name.module.ts
    └── feature-name-routing.module.ts
    
  3. Add lazy-loaded route in app-routing.module.ts:
    {
      path: 'feature-name',
      loadChildren: () => import('./features/feature-name/feature-name.module')
        .then(m => m.FeatureNameModule)
    }

Adding NgRx State

  1. Create state interface in store/state/
  2. Create actions in store/actions/
  3. Create reducer in store/reducers/
  4. Create effects in store/effects/ (if needed)
  5. Register in app.module.ts

Adding Shared Components

  1. Create component in shared/components/
  2. Declare and export in shared.module.ts
  3. Import SharedModule in feature modules that need it

Performance Optimization

  • Lazy Loading: All feature modules are lazy-loaded
  • OnPush Strategy: Components use OnPush change detection
  • Tree Shaking: Barrel exports help with tree shaking
  • Bundle Analysis: Use ng build --stats-json to analyze bundles

Code Splitting

  • Feature modules are automatically code-split
  • Shared module is in a separate chunk
  • Consider route-based code splitting for large features

Testing

This project uses Vitest for unit testing and Playwright for E2E testing. The test suite demonstrates comprehensive testing practices for Angular applications.

Why Vitest Instead of Jest?

This project migrated from Jest to Vitest for several important reasons:

Performance & Speed:

  • Faster execution: Vitest is built on Vite, providing significantly faster test runs, especially in watch mode
  • Native ESM support: Better compatibility with modern JavaScript modules
  • Optimized for development: Instant feedback during test-driven development

Better Angular Integration:

  • Modern tooling: Vitest works seamlessly with Angular 21 and TypeScript 5.9
  • Improved compatibility: Better handling of Angular's dependency injection and TestBed
  • Future-proof: Active development and better alignment with Angular's direction

Developer Experience:

  • Interactive UI: Built-in test UI (npm run test:ui) for visual test exploration
  • Better error messages: More informative error reporting
  • Hot module replacement: Faster test re-runs during development

Technical Advantages:

  • Smaller bundle size: More efficient than Jest for modern projects
  • Better TypeScript support: Native TypeScript support without additional configuration
  • Consistent tooling: Uses the same build tool (Vite) as modern Angular tooling

Migration Benefits:

  • All 31 tests migrated successfully with improved async handling
  • Better integration with Angular's testing utilities
  • More maintainable test setup with cleaner configuration

Test Suite Overview

The project includes 31 unit tests across 3 test suites, all passing:

1. ApiService Tests (7 tests)

Tests the core HTTP communication service that provides a centralized API layer:

  • ✅ Service instantiation and dependency injection
  • ✅ GET requests with URL construction and query parameters
  • ✅ POST requests with request body handling
  • ✅ PUT requests for full resource updates
  • ✅ DELETE requests for resource removal
  • ✅ PATCH requests for partial updates

Key Features Demonstrated:

  • HTTP request mocking with HttpClientTestingModule
  • Async operation testing with proper promise handling
  • Request verification (method, URL, body, query params)
  • Environment-based URL configuration

2. AuthService Tests (9 tests)

Tests the authentication service managing user authentication state:

  • ✅ Service instantiation
  • ✅ Initial state detection (authenticated/unauthenticated)
  • ✅ Login functionality with token storage
  • ✅ Reactive state management with RxJS Observables
  • ✅ Logout functionality with token removal and navigation
  • ✅ Token retrieval from localStorage

Key Features Demonstrated:

  • localStorage integration testing
  • RxJS Observable state management
  • Router navigation mocking
  • Reactive state emission verification

3. ButtonComponent Tests (15 tests)

Tests a reusable presentational button component:

  • ✅ Component instantiation
  • ✅ Input property configuration (type, variant, disabled, loading)
  • ✅ Event emission handling
  • ✅ Conditional rendering (loading spinner)
  • ✅ Template rendering and DOM output
  • ✅ Accessibility features (disabled states)

Key Features Demonstrated:

  • Presentational component pattern (Smart/Dumb)
  • Input/Output property testing
  • Template rendering verification
  • Change detection with OnPush strategy
  • Conditional DOM rendering

Testing Approach

  • Unit Testing: Each service and component is tested in isolation
  • Mocking: HTTP calls, Router, and localStorage are properly mocked
  • Async Handling: Uses async/await with RxJS firstValueFrom for Observable-based code
  • Change Detection: Properly handles Angular's change detection in component tests
  • Test Isolation: Each test runs in a clean state with proper setup/teardown

Quick Start

# Run all unit tests
npm test

# Run tests in watch mode (auto-reruns on file changes)
npm run test:watch

# Run tests with coverage report
npm run test:coverage

# Open interactive test UI
npm run test:ui

# Run E2E tests
npm run test:e2e

# Run E2E tests with UI
npm run test:e2e:ui

Test Coverage

Run npm run test:coverage to generate a detailed coverage report showing:

  • Statement coverage
  • Branch coverage
  • Function coverage
  • Line coverage

See TESTING.md for detailed testing documentation and best practices.

Additional Resources

License

This project is for demonstration purposes.

About

testing angular

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors