Beliebte Suchanfragen
//

Angular Signals

23.3.2025 | 15 Minuten Lesezeit

Übersicht zu Angular Signals

Mit Signals liefert Angular eine weitere Möglichkeit für ein reaktives Zustandsmanagement.

Für Umsteiger:innen aus z. B. React bietet die neue Signals-API eine gute und bekannte Möglichkeit Zustände darzustellen und zu verwalten.

Signals kann man als Wrapper um einen Wert verstehen, dessen Zustand Angular intern überwacht und den neuen Zustand vom Wert an die jeweiligen Interessenten weitergibt.

Dabei unterscheiden wir Signals in Writable und Readable Signals, d. h. in Signals, die beschrieben werden können und somit neue Werte erhalten und lediglich lesbare Signale, dessen Werte und Änderungen nur ausgelesen werden.

Writable and Readable Signals

Die Angulars Signals API bietet rein syntaktisch bereits eine Unterscheidung für die Erkennung der jeweiligen Signale.

Writable Signals werden mit Hilfe der signals Funktion wie folgt erstellt:

1const value = signal<number>(0)

Nachdem das writable Signal erstellt wurde, stellt die API für die Veränderung des Wertes Funktionen bereit - zum Setzen mit set, Updaten mit update oder das Signal nach Außen als Readable mit asReadonly .

1const value = signal<number>(0)
2
3value.set(3) 
4value() // Aufruf value() liefert 3
5
6value.update(prev -> prev + 1) 
7value() // Aufruf von value() liefert 4
8
9const readableSignal = value.asReadonly() // Variable ist nicht veränderbar

Am Beispiel sind die Aufrufe bzw. das Auslesen der Signal-Werte mit einem Kommentar versehen, mit dem aktuellen Wert. Dabei ist es wichtig zum Aufruf der Werte die Signal-Variable als Funktion aufzurufen, um den dahinter liegenden Wert zu erhalten.

Computed Signals

Computed Signals gehören zu den readable Signals und können als abgeleitete Werte verstanden werden, d. h. der Wert von einem computed Signal wird in Abhängigkeit von einem vorhandenen Signal oder Signals ermittelt.

1const writableSignal = signal<number>(0)
2
3const derivedSignal = computed(() => {
4    const dependedValue = writableSignal()
5    return dependedValue + 1
6})
7
8writableSignal.set(1) // Wert wird auf 1 verändert
9
10derivedSignal() // durch Änderung wird neuer Wert ermittelt -> 2

Im obigen Beispiel habe ich bewusst die Benennung der Variablen als solche gewählt, um die Zusammenhänge besser zu verdeutlichen.

Denn bei computed Signals ist es erforderlich die abhängige Größe(n), hier writableSignal im Funktionskörper computed aufzurufen. Dadurch wird Angular vermittelt, welche Abhängigkeiten zum abgeleiteten Wert derivedSignal bestehen, um somit den Wert neu zu ermitteln, sobald sich die Abhängigkeit writableSignal ändert.

Seiteneffekte mit effect

Für die Umsetzung von Seiteneffekten, darunter versteht man im Angular Kontext zusätzliche Operationen einer Hauptfunktion, die den Zustand der Anwendung verändern, z. B. beim Update vom Signal werden in der UI Werte geändert und angezeigt, gibt es die effect Operation. Anders als computed gibt effect keinen Wert für ein neues Signal zurück.

Die effect Operation wird in einem sog. Injection Kontext ausgeführt, d. h. im Konstruktor der jeweiligen Klasse, die instanziiert wird.

1@Component({/*...*/})
2export class SimpleAdditionComponent {
3    private state = signal<number>(0)
4
5    constructor() {
6        effect(() => {
7            const dependency = this.state()
8            console.log(`sideeffect of state: ${this.state()}`)
9        })
10    }
11
12    addOne() {
13        this.state.update(prev => prev + 1)
14    }
15}

Im obigen Beispiel wird die effect Operation in dem Injection Kontext, dem Konstruktor der Klasse aufgerufen, um den jeweiligen Seiteneffekt darzustellen, sobald sich die Abhängigkeit state ändert. Aus dem Anlass habe ich die Variable im Funktionskörper der effect Operation dependency genannt, um auf die Abhängigkeit aufmerksam zu machen.

Wichtig ist, dass die effect Operation beim ersten Durchgang immer ausgeführt wird, sobald die Instanziierung der Klasse erfolgt. In den nächsten Fällen wird der Seiteneffekt erst vollzogen, sollte die addOne Methode aufgerufen werden. Die Ausführung der effect Operation erfolgt asynchron.

Ebenso sollte beachtet werden, dass innerhalb der effect Operation Werte von Signalen gesetzt werden können. Jedoch empfehle ich in einem solchen Fall auf die Abhängigkeiten zu achten, weil es im Falle einer zyklischen Abhängigkeit zu einer Endlosschleife führt.

D. h. sollte das state Signal innerhalb der effect Operation einen neuen Wert erhalten, folgt es zu einem erneuten Aufruf des Funktionskörpers.

input Signal

Neben den o. a. Signals für die Darstellung und Ableitung von Zuständen gibt es die Möglichkeit Signals als Daten in eine Komponente einzubringen, von einer Eltern-Komponente in eine Kind-Komponente.

Mit dem input Signal innerhalb der Komponente wird die frühere Variante @Input als Annotation und Mittel zur Komponenten-Kommunikation abgelöst.

Der Vorteil liegt im Vergleich zur @Input Variante u. a. bei der Type-Safety, d. h. die übergebenen Werte haben einen zugewiesenen Typen und TypeScript gibt Feedback, sollten Diskrepanzen vorliegen.

1@Component({/*...*/})
2export class DisplayComponent {
3    public displayValue = input<Data>() // Datentyp: Data oder undefinded
4}
5
6// im Template
7@if(displayValue()) {
8    <div>{{displayValue()}}</div>
9} @else {
10    <div>no data to display</div>
11}

Im o. a. Beispiel ist die input Signal Variante in der Klassen-Komponente und im Template der dazugehörigen Komponente dargestellt.

Hierbei fällt auf, dass die Eingabe von Daten mittels des input Schlüsselwortes umgesetzt und im Template als Funktionsaufruf vom Signal angezeigt werden kann. Bei der Nutzung von Signals sollte darauf geachtet werden diese zu initiieren, denn sollte, wie im Beispiel das Signal nicht initiiert werden, enthält es zunächst den Wert undefined.

Aus dem Anlass habe ich im Beispiel die Syntax für den Kontrollfluss im Template mit @if und @else genutzt, denn displayValue soll nur angezeigt werden, wenn der Input eingegeben wurde.

Die input API gibt uns darüber hinaus die Möglichkeit Eingaben als Pflichtangaben zu deklarieren, dafür gibt es die Punktoperation required.

1@Component({/*...*/})
2export class DisplayComponent {
3    public displayValue = input.required<Data>()
4}
5
6// im Template
7<div>{{displayValue()}}</div>

Ich habe hierfür das Beispiel mit der required Punktoperation zum input angepasst. Beim Vergleich der Beispiele ist ersichtlich, dass die Syntax für den Kontrollfluss im Template ausfällt.

Damit wird die Nutzung der DisplayComponent erst möglich, wenn die displayValue als Input übergeben wird. Aus dem Anlass braucht das Input-Signal im Klassen-Körper der DisplayComponent keine Initiierung innerhalb der runden Klammern.

Sollte die DisplayComponent ohne den geforderten Input genutzt werden, wird Angular einen Fehler auswerfen.

output Signal

Im Rahmen der Komponenten-Kommunikation existiert die Möglichkeit Informationen von der Kind-Komponente an die Eltern-Komponente weiter zu leiten.

Die output Signal API dient damit als Ersatz für die bisherige @Output Annotation, mit gleichen Vorteilen, wie input - die Typisierung zwischen den Komponenten sind klar und überprüfbar, im Gegenteil zur bisherigen @Output Annotation.

1// Child-Component
2@Component({/*...*/})
3export class ListItemComponent {
4    item = input.required<Item>()
5    itemSelected = output<Item>()
6
7    selectItem(item: Item) {
8        this.itemSelected.emit(item)
9    }
10}
11
12// Template of child-component
13<li>
14    <p>{{item().name}}</p>
15    <button (onClick)="selectItem(item())">Add Item</button>
16</li>
17
18// Parent-Component
19@Component({/*...*/})
20export class ListComponent implements OnInit {
21    private itemService = inject(ItemService)
22    private itemsState = signal<Item[]>([])
23
24    public itemsSignal = this.itemsState.asReadOnly()
25
26    ngOnInit() {
27        this.itemService.getItems().subscribe((items: Item[]) => {
28            this.itemsState.set(items)
29        })
30    }
31
32    addItem(item: Item) {
33        this.itemService
34            .addItem(item)
35            .subscribe(...)
36    }
37}
38
39// Template of parent-component
40<ul>
41    @for(item of itemsSignal(); track item.id) {
42        <app-list-item [item]="item" (itemSelected)="addItem($event)" />
43    }
44</ul>

Im obigen Beispiel sind die bisherigen Themen beziehungsweise Funktionalitäten eingearbeitet und mit dem output Signal verknüpft.

Dabei wird zunächst eine Liste von Elementen von der Elternkomponente an die Kind-Komponente mit Hilfe von input weitergereicht und innerhalb des Templates angezeigt.

Mit Hilfe der output API wird das gewählte Element innerhalb der selectItem Funktion mit this.itemSelected.emit(...) an die Elternkomponente emittiert.

Die Elternkomponente fängt das emittierte Event (itemSelected) von der Kindkomponente mit addItem=($event) auf und verarbeitet den Wert vom Output innerhalb der addItem Methode.

Innerhalb der addItem Methode wird die Auswahl des Elementes mit der API des ItemService vorgenommen und verarbeitet.

Am konkreten Beispiel

Zur Veranschaulichung der bisherigen Themen, habe ich eine konkrete Anwendung ausgearbeitet.

Bei der Anwendung handelt es sich um eine E-Commerce Anwendung, die aus unterschiedlichen Komponenten besteht - Produkte auflistet, eine detaillierte Produktseite anzeigt und einen Warenkorb besitzt.

Es besteht die Möglichkeit zwischen der Produktauflistung, den Details und dem Warenkorb zu navigieren sowie Produkte in den Warenkorb hinzuzufügen.

Für das Beispiel nutze ich Angular (Version 19), die Komponenten Bibliothek PrimeNG und TailwindCSS.

Zur Auslieferung von den notwendigen Daten habe ich für Test-Zwecke ein Fastify Backend zur Verfügung gestellt, das sich nicht im folgenden Repository befindet.

Das Projekt befindet sich im folgenden Repositiory: Github - Angular Signals

Produktliste und Listenelemente (input, output)

Im folgenden Screenshot ist die Hauptseite und darin die Auflistung der einzelnen Produkte zu sehen.

Die Auflistung der Produkte und die einzelnen Produkt-Positionen (z. B. Wireless Bluetooth Headphones) sind als zwei Komponenten designed.

Als Eltern-Komponente (ProductOverview) dient die Auflistung, die den Service zum Einholen der Produkte nutzt. Darüber hinaus gibt die Eltern-Komponente die einzelnen Produkte weiter an die Kind-Komponente (Product-Item), zur Darstellung des jeweiligen Produktes.

1// Auszug Template Eltern-Komponente (angepasst fürs Beispiel)
2@for (product of products(); track product.id) {  
3  <sgn-product-item [product]="product" (cartItemAdded)="addItemToCart($event)" />    
4}
5
6// Auszug Eltern-Komponente (angepasst fürs Beispiel)
7@Component({/*...*/})  
8export class ProductOverviewComponent {  
9  private productService = inject(ProductService);  
10  private cartService = inject(CartService);    
11  
12  private productState = signal<Product[]>([]);  
13  public products = this.productState.asReadonly();  
14  
15  constructor() {  
16    this.productService  
17      .getProducts()  
18      .pipe(takeUntilDestroyed())  
19      .subscribe((products: Product[]) => {  
20        this.productState.set(products);  
21      });  
22  }  
23  
24  addItemToCart(cartItem: CartItem): void {  
25    this.cartService.addItemToCart(cartItem).subscribe({/*...*/});  
26  }  
27}

Im Template ist die Kind-Komponente mit dem Selektor sng-product-item ersichtlich, den das Produkt von der Eltern-Komponente weitergereicht wird.

1@Component({  
2  selector: 'sgn-product-item',  
3  /*...*/
4})  
5export class ProductItemComponent {  
6  public product = input.required<Product>();  
7  public cartItemAdded = output<CartItem>();  
8  
9  addCartItem(product: Product) {  
10    this.cartItemAdded.emit({ product, amount: 1 });  
11  }  
12}

Innerhalb der Kind-Komponente wird das input sowie output Signal genutzt, um die Eingabe vom Produkt sowie emittieren an die Eltern-Komponente zu realisieren.

Mit der Nutzung von diesen Signals ist es gelungen, die Daten an einem Ort zu fetchen und weiterzureichen, an eine sog. Presentational-Component, die lediglich Daten präsentiert.

Warenkorb (effect, computed)

Am Beispiel vom Warenkorb werde ich eine mögliche Nutzung von effect und computed aufzeigen. Wir werden innerhalb des effect Blockes versuchen einen Service zu subscriben und die erhaltenen Daten in ein Signal, den cartSate, zu schreiben.

Aus den Produkten von Warenkorb, die von der cartState Variable gehalten werden, leiten wir die Summe ab und bedienen uns damit dem computed Signal.

Im obigen Auszug ist der Warenkorb, mit den einzelnen Produkten sowie der Gesamtsumme sichtbar.

1@Component({/*...*/})  
2export class CartOverviewComponent {  
3  private cartService = inject(CartService);  
4  private cartState = signal<Cart | null>(null);  
5  public cartSignal = this.cartState.asReadonly();  
6  
7  public cartSum = computed(() => {  
8    const cart = this.cartSignal();  
9    if (cart) {  
10      return this.getSumOfCart(cart);  
11    } else {  
12      return null;  
13    }  
14  });  
15  
16  constructor() {  
17    effect(() => {  
18      this.cartService.getCart().subscribe((cart) => {  
19        this.cartState.set(cart);  
20      });  
21    });  
22  }    
23}

Innerhalb der CartOverviewComponent wird im constructor Block das effect Signal genutzt, um die Produkte im Warenkorb zu fetchen und sie anschließend im Warenkorb-Zustand cartState festzuhalten.

Das cartSignal, was lediglich von Außen lesbar ist (public), wird im Template zur Veranschaulichung genutzt. Aus dem cartState, in dem sich alle Produkte im Warenkorb befinden, wird die Summe abgeleitet und ebenfalls im Template angezeigt.

Bei der Nutzung von effect zum Beschreiben von Signalen sollte stets darauf geachtet werden zirkuläre Abhängigkeiten zu vermeiden.

Alternative mit resource

effect ist eine Möglichkeit Seiteneffekte i. V. m. Services umzusetzen, jedoch nicht die von Angular Signals favorisierte Variante, auch wenn sie in der Praxis gerne genutzt wird.

Im Angular v19 Update wurde das resource Signal eingeführt, um explizit mit asynchronen Operationen besser umzugehen.

Ich habe das obige Beispiel der CardOverviewComponent abgeändert und den Aufruf des Service innerhalb vom resource Block umgesetzt.

1@Component({/*...*/})  
2export class CartOverviewComponent {  
3  private cartService = inject(CartService);  
4  public cartSignal = computed(() => this.cartResource.value());  
5  
6  public cartSum = computed(() => {  
7    const cart = this.cartSignal();  
8    if (cart) {  
9      return this.getSumOfCart(cart);  
10    } else {  
11      return null;  
12    }  
13  });  
14  
15  public cartResource = resource({  
16    loader: async () => {  
17      return await firstValueFrom(this.cartService.getCart());  
18    }  
19  });   
20}

Innerhalb vom loader Block des resource Signals wird der cartService aufgerufen. Zu beachten ist, dass der loader Block ein Promise als Rückgabe erwartet, weshalb ich im Beispiel firstValueFrom der rxjs Bibliothek nutze. Damit wird ein Observable in ein Promise konvertiert und resolved, sobald der erste Wert von der Subscription angekommen ist.

An diesem kleinen Beispiel wollte ich den Wert und Nutzen von resource aufzeigen und wie es eingesetzt werden kann.

Testen von Signals

Das Testen von Signals werde ich anhand der konkreten Beispiele mit den eingesetzten Signal APIs vorstellen und dabei auf wesentliche Aspekte eingehen.

Ich möchte in dem Rahmen darauf aufmerksam machen, dass ich beim Anlegen vom Projekt vitest und die Angular Testing Library nutze. Unabhängig von der gewählten Technologie, werde ich ebenfalls die Testfälle exemplarisch mit TestBed veranschaulichen.

Zur einheitlichen Struktur und Verständnis sind die Tests nach dem Arrange-Act-Assert Pattern angelegt.

Testing input, output

Im Testfall für das input und output Signal ziehe ich die ProductItemComponent als Beispiel heran.

In der obigen Abbildung ist die Komponente zu sehen, die sich als Kind-Komponente in der bereits gezeigten ProductOverviewComponent befindet.

Aufbau

Die ProductItemComponent ist folgend aufgebaut:

1// Class
2@Component({/*...*/})  
3export class ProductItemComponent {  
4  public product = input.required<Product>();  
5  public cartItemAdded = output<CartItem>();  
6  
7  addCartItem(product: Product) {  
8    this.cartItemAdded.emit({ product, amount: 1 });  
9  }  
10}
11
12// Template
13<div class="...">  
14  {/*...*/}
15  <div class="...">  
16    <a [routerLink]="[product().id]" class="...">  
17      <p class="...">{{ product().category }}</p>  
18      <p class="..." data-testid="product-name">{{ product().name }}</p>  
19    </a>  
20    @if (product().rating) {  
21      <p-rating stars="5" [ngModel]="product().rating" readonly />  
22    }  
23  </div>  
24  <div class="...">  
25    <p class="...">{{ product().price | currency }}</p>  
26    <p-button  
27      data-testid="add-to-cart-button"  
28      icon="..."  
29      label="Add to cart"  
30      (onClick)="addCartItem(product())"  
31    ></p-button>  
32  </div>  
33</div>
Variante mit Angular Testing Library

Die Angular Testing Library stellt zum Testen von Komponenten mit Signals die Property inputs zur Verfügung, die sich im Körper der render Funktion befindet.

1// Testing input signal
2it('should display product item', async () => {  
3  await render(ProductItemComponent, {  
4    inputs: {  
5      product: productMock  
6    }  
7  });  
8  
9  expect(screen.getByText(productMock.name)).toBeTruthy();  
10});

Im obigen Unit-Test wird geprüft, ob das eingegebene Produkt im Template sichtbar ist. Dabei nutze ich stichprobenartig den Selektor getByText der Angular Testing Library und erwarte den Produktnamen im Template.

Beim Testen von dem output Signal nutze ich einen sog. Spy, um auf das emit vom Output zu hören.

1it('should add item to cart', async () => {  
2  const { fixture } = await render(ProductItemComponent, {  
3    inputs: {  
4      product: productMock  
5    }  
6  });  
7  const spy = vi.spyOn(fixture.componentInstance.cartItemAdded, 'emit');  
8  
9  screen.getByText('Add to cart').click();  
10  
11  expect(spy).toHaveBeenCalledWith({  
12    amount: 1,  
13    product: productMock  
14  });  
15});

Mittels der destructuring assignment ({fixture}) wird das sog. fixture zum Testen und Debugging der Komponente aus der render Funktion ausgepackt und im spyOn (Spionier-Funktion) verwendet, um auf das emit des cartItemAdded Outputs zu hören.

Variante mit TestBed

Beim Testen mit TestBed gibt es Ähnlichkeiten bei den Unit-Tests, wie im folgenden zu sehen ist:

1// Testing: input signal
2it('should display product item', () => {  
3  const productTitleElement = debugElement.query(  
4    By.css('[data-testid="product-name"]')  
5  ).nativeElement;  
6  
7  expect(productTitleElement.textContent).toStrictEqual(productMock.name);  
8});  
9
10// Testing: output signal
11it('should add item to cart', () => {  
12  const spy = vi.spyOn(fixture.componentInstance.cartItemAdded, 'emit');  
13  const addToCartButton = debugElement.query(By.css('[data-testid="add-to-cart-button"]'));  
14  
15  addToCartButton.triggerEventHandler('onClick');  
16  fixture.detectChanges();  
17  
18  expect(spy).toHaveBeenCalledWith({  
19    amount: 1,  
20    product: productMock  
21  });  
22});

Das Vorgehen bei den beiden Signals ist ähnlich zu den Tests mit der Angular Testing Library. Jedoch unterscheiden sie sich bei der Bereitstellung der Test-Komponente und der damit zusammenhängenden Konfiguration.

Die Konfiguration von der Test-Komponente erfolgt mit der TestBed Variante im beforEach Block und sieht folgendermaßen aus:

1beforeEach(async () => {  
2  await TestBed.configureTestingModule({  
3    imports: [ProductItemComponent],  
4    providers: [/*...*/]  
5  }).compileComponents();  
6  
7  fixture = TestBed.createComponent(ProductItemComponent);  
8  
9  /* zwei optionen zum setzen vom komponenten input */
10  // Option 1  
11  fixture.componentRef.setInput('product', productMock);
12
13  // Option 2
14  TestBed.runInInjectionContext(() => {  
15    fixture.componentInstance.product = input(productMock);  
16  });  
17  
18  fixture.detectChanges();  
19  
20  debugElement = fixture.debugElement;  
21});

Innerhalb vom TestBed.configureTestingModule wird die Komponente konfiguriert und das Fixture als Testschnittstelle zugewiesen.

Bei der Übergabe vom input Signal gibt es zwei mögliche Optionen, die ich aufgeführt habe. Die zweite Option bietet den Vorteil, dass die Typisierung erhalten und angezeigt wird.

Testing computed und effect

Beim Testen von computed und effect wird das Beispiel der CardOverviewComponent herangezogen.

In dem Komponenten-Test möchte ich darauf aufmerksam machen, dass es sich hierbei um einen Test mit ChangeDetection handelt, anders als beim Testen von Signals in Services. Damit ist es nicht möglich, wie in den vorherigen Beispielen fixture.detectChanges aufzurufen.

Um ausstehende Signals bzw. Änderungen auszuführen, steht der TestBed API die flushEffects Funktion zu Verfügung.

In der unten dargestellten Abbildung ist die Test-Komponente CardOverviewComponent zu sehen, mit den jeweiligen Items und der Gesamtsumme.

Aufbau

Die CardOverviewComponent ist folgendermaßen aufgebaut:

1// Class
2@Component({/*...*/})  
3export class CartOverviewComponent {  
4  private cartService = inject(CartService);  
5  private cartState = signal<Cart | null>(null);  
6  public cartSignal = this.cartState.asReadonly();  
7  
8  public cartSum = computed(() => {  
9    const cart = this.cartSignal();  
10    if (cart) {  
11      return this.getSumOfCart(cart);  
12    } else {  
13      return null;  
14    }  
15  });  
16  
17  constructor() {  
18    effect(() => {  
19      this.cartService.getCart().subscribe((cart) => {  
20        this.cartState.set(cart);  
21      });  
22    });  
23  }  
24  
25  private getSumOfCart(cart: Cart): number {  
26    return cart.items.reduce((prev, curr) => {  
27      return prev + curr.product.price;  
28    }, 0);  
29  }  
30  
31  deleteProductById(id: number) {  
32    this.cartService.deleteItemFromCart(id.toString()).subscribe(() => {  
33      this.cartService.getCart().subscribe((cart) => {  
34        this.cartState.set(cart);  
35      });  
36    });  
37  }  
38}
Variante mit Angular Testing Library
1describe('CartOverviewComponent', () => {  
2  const cartMock = CartBuilder.build();  
3  const cartServiceMock: Partial<CartService> = {  
4    getCart: () => of(cartMock),  
5    deleteItemFromCart: (id: string) => {  
6      return of({});  
7    }  
8  };  
9  
10  beforeEach(async () => {  
11    await render(CartOverviewComponent, {  
12      providers: [{ provide: CartService, useValue: cartServiceMock }]  
13    });  
14  });  
15  
16  it('should display item in cart', async () => {  
17    const productName = cartMock.items[0].product.name;  
18  
19    expect(screen.getByText(productName)).toBeTruthy;  
20  });  
21  
22  it('should display total of cart with one item', async () => {  
23    const cartPrice = cartMock.items[0].product.price;  
24  
25    expect(screen.getByTestId('cart-sum')).toBeTruthy;  
26    expect(screen.getByTestId('cart-sum').innerHTML).contains('total', `$${cartPrice}`);  
27  });  
28});

Im obigen Ausschnitt habe ich den Test mit Hilfe der Angular Testing Library vorgenommen, dabei habe ich die render Funktion innerhalb des beforeEach Blocks platziert.

Alle sonstigen Tests ähneln den vorherigen Beispielen, mit der vorliegenden ChangeDetection innerhalb von Komponenten-Tests ist es nicht notwendig ausstehende Effekte auszuführen.

Variante mit TestBed

Zur Vollständigkeit folgt die Umsetzung mit TestBed:

1describe('CartOverviewComponent', () => {  
2  let fixture: ComponentFixture<CartOverviewComponent>;  
3  let debugElement: DebugElement;  
4  
5  const cartMock = CartBuilder.build();  
6  const cartServiceMock: Partial<CartService> = {  
7    getCart: () => of(cartMock)  
8  };  
9  
10  beforeEach(async () => {  
11    await TestBed.configureTestingModule({  
12      imports: [CartOverviewComponent],  
13      providers: [{ provide: CartService, useValue: cartServiceMock }]  
14    }).compileComponents();  
15  
16    fixture = TestBed.createComponent(CartOverviewComponent);  
17    debugElement = fixture.debugElement;  
18  
19    fixture.detectChanges();  
20  });  
21  
22  it('should display item in cart', () => {  
23    const cartItemName = debugElement.query(By.css('[data-testid="cart-item-name"]')).nativeElement;  
24  
25    expect(cartItemName.textContent).contain(cartMock.items[0].product.name);  
26  });  
27  
28  it('should display total of cart with one item', () => {  
29    const cartPrice = cartMock.items[0].product;  
30    const sum = debugElement.query(By.css('[data-testid="cart-sum"]')).nativeElement;  
31  
32    expect(sum).toBeTruthy;  
33    expect(sum.textContent).contains('total', `$${cartPrice}`);  
34  });  
35});

Fazit

Die Nutzung von Signals innerhalb der Angular Anwendung, um Inputs, Outputs sowie Seiteneffekte von Zuständen darzustellen, ist mit Hilfe der Dokumentation und der übersichtlichen Schnittstelle relativ schnell zu erlernen und anzuwenden.

Darüber hinaus finden Signals innerhalb von anderen Frameworks oder Libraries Anwendung und erleichtert aufgrund der Ähnlichkeit den Umstieg auf Angular.

Angulars Weg zur Vereinfachung und einheitlicheren Gestaltung vom Zustandsmanagement nimmt mit jedem Update mehr gestalt an und ersetzt sukzessiv die bisher bekannten Pattern mit RxJs. Ein gutes Beispiel für die stetige Weiterentwicklung zeigt die Variante mit resource bei der CardOverviewComponent, um die Ressourcen für den Warenkorb vom dazugehörigen Backend-System zu laden.

Quellen und Informationen

Angular Documentation to Signals

Angular-University-Blog Guide to Signals

Blog Rainer Hahnenkamp - Testing Signals

Beitrag teilen

//
Jetzt für unseren Newsletter anmelden

Alles Wissenswerte auf einen Klick:
Unser Newsletter bietet dir die Möglichkeit, dich ohne großen Aufwand über die aktuellen Themen bei codecentric zu informieren.