Blogs Mastering Angular Signals and Inputs: A Practical Guide with Jest Testing

Mastering Angular Signals and Inputs: A Practical Guide with Jest Testing

December 2, 2024

loading

Angular Signals, introduced in version 16, represent a powerful evolution in state management. In this article, we'll explore how to effectively combine Signals with Inputs while ensuring robust testing with Jest. We'll build a practical example that demonstrates these concepts in action.

Understanding the Basics

Before diving into the implementation, let's briefly review what makes Signals special. Unlike traditional reactive programming in Angular, Signals provide a more granular and efficient way to handle reactivity. They only update components when their specific dependencies change, leading to better performance.

Our Example: A Dynamic Product Card Component

We'll create a product card component that uses both Signals and Inputs, making it both reactive and reusable. This component will display product information and handle inventory updates in real-time.

The Component Implementation

product-card.component.ts
typescript
import { Component, Input, computed, signal } from '@angular/core';

interface Product {
  id: number;
  name: string;
  price: number;
  inventory: number;
}

@Component({
  selector: 'app-product-card',
  template: `
    <div class="product-card">
      <h2>{{ product().name }}</h2>
      <p>Price: ${{ product().price }}</p>
      <p>In Stock: {{ availabilityMessage() }}</p>
      <button 
        [disabled]="!isAvailable()" 
        (click)="decrementInventory()"
      >
        Purchase
      </button>
    </div>
  `
})
export class ProductCardComponent {
  @Input({ required: true }) set initialProduct(value: Product) {
    this.product.set(value);
  }

  product = signal<Product>({
    id: 0,
    name: '',
    price: 0,
    inventory: 0
  });

  isAvailable = computed(() => this.product().inventory > 0);

  availabilityMessage = computed(() => 
    this.product().inventory > 0 
      ? `${this.product().inventory} units` 
      : 'Out of stock'
  );

  decrementInventory() {
    if (this.isAvailable()) {
      this.product.update(current => ({
        ...current,
        inventory: current.inventory - 1
      }));
    }
  }
}

Writing Jest Tests

Now, let's write comprehensive tests for our component. We'll test both the initial state and reactive behaviors.

product-card.component.cy.ts
undefined
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProductCardComponent } from './product-card.component';

describe('ProductCardComponent', () => {
  let component: ProductCardComponent;
  let fixture: ComponentFixture<ProductCardComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ ProductCardComponent ]
    }).compileComponents();

    fixture = TestBed.createComponent(ProductCardComponent);
    component = fixture.componentInstance;
  });

  const mockProduct = {
    id: 1,
    name: 'Test Product',
    price: 99.99,
    inventory: 5
  };

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should initialize with the provided product', () => {
    component.initialProduct = mockProduct;
    expect(component.product()).toEqual(mockProduct);
  });

  it('should compute availability correctly', () => {
    component.initialProduct = mockProduct;
    expect(component.isAvailable()).toBeTruthy();

    // Simulate purchasing all items
    for (let i = 0; i < mockProduct.inventory; i++) {
      component.decrementInventory();
    }
    
    expect(component.isAvailable()).toBeFalsy();
  });

  it('should update availability message based on inventory', () => {
    component.initialProduct = mockProduct;
    expect(component.availabilityMessage()).toBe('5 units');

    component.decrementInventory();
    expect(component.availabilityMessage()).toBe('4 units');

    // Set inventory to 0
    for (let i = 0; i < 4; i++) {
      component.decrementInventory();
    }
    expect(component.availabilityMessage()).toBe('Out of stock');
  });

  it('should not decrement inventory when out of stock', () => {
    component.initialProduct = { ...mockProduct, inventory: 0 };
    component.decrementInventory();
    expect(component.product().inventory).toBe(0);
  });
});

Key Testing Insights

Testing Signals: When testing Signals, remember to use the function call syntax (product()) to access the current value. This is different from testing regular properties.

Input Testing: We test the @Input setter by directly assigning values to initialProduct and verifying that the Signal is updated correctly.

Computed Properties: Testing computed properties is straightforward - we can access them like methods and verify their output based on the current Signal state.

State Changes: We test state changes by calling methods that update the Signals and verifying both the direct changes and their effects on computed properties.

Best Practices

Initialize Signals with Default Values: Always provide default values for your Signals to avoid undefined states.

Use Computed for Derived State: Instead of creating multiple Signals, use computed() for values that can be derived from existing Signals.

Immutable Updates: When updating Signal values, create new objects instead of mutating existing ones.

Test Edge Cases: Include tests for boundary conditions and error cases, such as empty or invalid inputs.

Conclusion

Combining Angular Signals with Inputs creates powerful, reactive components that are also easily testable. By following the patterns shown in this article, you can create robust, reactive Angular applications while maintaining high test coverage.

The key benefits of this approach include:

  • Fine-grained reactivity with Signals
  • Clear component interfaces with Inputs
  • Comprehensive testing capabilities
  • Better performance through targeted updates

Remember that Signals are still evolving, and Angular may introduce new features and best practices in future versions. Stay tuned to the official Angular documentation for updates and new capabilities.

Thanks for reading!

loading

Comments

view source
TypeScript 37.9%
C# 18.8%
CSS 15.9%
Svelte 15.4%
JavaScript 5.5%
Dockerfile 4.4%
Other 2.1%