Make Sure You Do This Before Switching To Signals
Tags: testing, angular, signal
Published: 2023-03-20 14:39:44

This time last year Standalone Component's were the talk of the Angular town. This year `signals` have replaced that buzz throughout the Angular Ecosystem. Though it will take some time for the introduction of `signals` into Angular's ecosystem to take hold, it is important for us to begin thinking about how these changes may or may not affect our applications. In fact, I am proposing **one thing that everyone should do before switching to `signals`**. But before I get to that, let's first learn what signals are. 

## What Are Signals Anyways?

> _For those interested in learning more, you can view [this PR](https://github.com/angular/angular/pull/49091) from the Angular team that introduces signals into Angular_.

For some historical context, `Signals` are not a new concept. In fact they are the backbone of reactivity in [SolidJS](https://www.solidjs.com/). [The SolidJS documentation](https://www.solidjs.com/tutorial/introduction_signals) describes _Signals_ as the following: "They contain values that change over time; when you change a signal's value, it automatically updates anything that uses it."

**Ok, but how are Signals different that what we currently have in Angular?** The simple answer for many is that they aren't much different from a practical standpoint. But if we dig a little deeper, we will find that Signals solve a long standing performance and architectural problem Angular has with it's current Change Detection mechanism. That mechanism is a library known as [Zone.Js](https://www.npmjs.com/package/zone.js?activeTab=readme). Though it works today, it has some pretty significant performance issues in larger applications that have a lot of changing state. By switching to Signals, we can slowly remove the parts of our applications that depend upon `Zone.js` and replace them with Signals. This will in effect improve the overall performance across our applications. 

**Ok, but what about RxJs?** RxJs is a huge part of the Angular Ecosystem! It is used for handling streams of **_asynchronous_** data in our applications. Thankfully, RxJs isn't going anywhere! Signals however, are being introduced to handle any and all _**synchronous**_ state changes in our Angular applications. I like to think of it as a replacement for any non RxJs state. 

#### In Short: 
- Async: RxJs
- Sync: Signals

## The Problem
Now that we know a little bit more about Signals and what problems they solve; let's talk about **the one thing you should do before switching to them**! Let's first look at the problem by looking at a simple example. 

Signals affect our Application's state, which in most cases is the most difficult and complex part of our applications. In fact, we want to be REALLY certain that any changes we make aren't causing regression. The best way to assure against this is through **automated testing**. More specifically **UI tests**. However, many unit tests written for Angular Components using Karma will break in the process of switching to `Signals`. This is because they are often times coupled to the implementation of the Component itself.

Let me show you can example below of a simple Standalone `CounterComponent`:

```ts
import { AsyncPipe } from '@angular/common'; 
import { Component } from '@angular/core'; 
import { BehaviorSubject } from 'rxjs'

@Component({ 
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <h3 id="count">Count: {{ count$ | async }}</h3>
    <div>
      <button id="decrement" (click)="decrement()">-</button>
      <button id="increment" (click)="increment()">+</button>
    </div>
  `
})
export class CounterComponent {
  private count = 0;
  private readonly _count = new BehaviorSubject(0);
  count$ = this._count.asObservable()

  increment(): void {
    this.count ++;
    this._count.next(this.count)
  }

  decrement(): void {
    this.count --;
    this._count.next(this.count)
  }
}
```

A typical Karma Unit Test for this component would look like the following:

```ts
import { ComponentFixture, TestBed } from '@angular/core/testing'; 
import { CounterComponent } from './counter.component';

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

  beforeEach(async () => {
    imports: [CounterComponent]
  }).compileComponents();

  fixture = TestBed.createComponent(CounterComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();

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

  it('can increment the count', () => {
    const compiled = fixture.nativeElement as HTMLElement;
    const count = compiled.querySelector("#count");
    expect(count?.textContent).toContain(0);
    const button = fixture.debugElement.nativeElement.querySelector('#increment')
    button.click();
    fixture.detectChanges();
    expect(count?.textContent).toContain(1);
  });

  it('can decrement the count', () => {
    const compiled = fixture.nativeElement as HTMLElement;
    const count = compiled.querySelector("#count");
    expect(count?.textContent).toContain(0);
    const button = fixture.debugElement.nativeElement.querySelector('#decrement')
    button.click();
    fixture.detectChanges();
    expect(count?.textContent).toContain(-1);
  })
});
```

Now we can run our test to validate it works by running:

```bash
ng test --watch
```

We should now seeing the following output:

![Counter Component Karma Output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7a211dctx92l454lou1v.png)

Now we can go ahead and refactor our component to use Signals:

```ts 
import { Component, signal } from '@angular/core'; 

@Component({ 
  standalone: true,
  template: `
    <h3 id="count">Count: {{ count() }}</h3>
    <div>
      <button id="decrement" (click)="decrement()">-</button>
      <button id="increment" (click)="increment()">+</button>
    </div>
  `
})
export class CounterComponent {
  count = signal(0)

  increment(): void {
    this.count.update(c => c = c + 1);
  }

  decrement(): void {
    this.count.update(c => c = c - 1);
  }
}
```
Now if we return to our test we will continue to see the following output:

![Counter Component Karma Output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7a211dctx92l454lou1v.png)

Though this example works, it isn't always this trivial. Let's revert those changes we just made and I will show you more complex unit tests that will eventually fail after switching to `signals`.

```ts
describe('CounterComponent', () => {
  let component: CounterComponent;
  let fixture: ComponentFixture<CounterComponent>;

  beforeEach(async () => {
    imports: [CounterComponent]
  }).compileComponents();

  fixture = TestBed.createComponent(CounterComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();

  it('increments count$ when calling increment()', () => {
    component.increment();
    component.count$.pipe(
        take(1)
    ).subscribe((value) => {
        expect(value).toEqual(1)
    })
  })

  it('decrements count$ when calling decrement()', () => {
    component.decrement();
    component.count$.pipe(
        take(1)
    ).subscribe((value) => {
        expect(value).toEqual(-1)
    })
  })
```

If we return to our RxJs implementation and run our tests we just wrote, we will see 5 successful tests passing. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0u76ihw75ogl3ai8ozk6.png)



But, if we refactor our `CounterComponent` to use signals again, we will now see the following error in our Code Editor. 

![VS Code Failure Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhia5nio4cqqtr2yfxpa.png)

As you can see, most tests that test the logic in our Component's class directly will inevitably fail after refactoring to signals. **To avoid this issue and improve the overall developer experience and quality of our tests, let's add Cypress Component Tests**

## Getting Started With Component Testing
If you haven't already done so let's add [Component Testing](https://docs.cypress.io/guides/component-testing/overview) to our application.

```bash
npm i cypress -D
```

Now we can launch Cypress and click on Component Testing:

```bash
npx cypress open
```

![Cypress Launchpad](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ji68qscgwlyqp826sllm.png)
 

Now you can simply follow Cypress's Configuration Wizard to setup Component Testing in your application (if you haven't already done so). You can follow my video below for a more detailed guide to getting started with Angular Component Testing.

{% embed https://www.youtube.com/embed/weWTHGWe6uU %}

## Writing Cypress Component Tests
Now that we have Cypress Component Testing configured let's create a Cypress Component test for the `CounterComponent`.

```ts
import { CounterComponent } from "./counter.component"

describe('CounterComponent', () => {
  it('can mount and display an initial value of 0', () => {
    cy.mount(CounterComponent)
    cy.get('#count').contains(0)
  })

  it('can increment the count', () => {
    cy.mount(CounterComponent)
    cy.get('#increment').click()
    cy.get('#count').contains(1)
  })

  it('can decrement the count', () => {
    cy.mount(CounterComponent)
    cy.get('#decrement').click()
    cy.get('#count').contains(-1)
  })
})
```

Now we can run the `counter.component.cy.ts` spec and we should get the following:

![Cypress Test Results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/56lwntebfkdzvx8qwc41.png)

Now let's go ahead and re-run our tests using the `signals` implementation and we will see the same result. Not only were the Cypress tests significantly **easier to write**, they also provide additional value that our Karma test runner did not. We are now able to interact with our component in our test runner itself and verify it's output _(as opposed to a tiny green dot)_.

> _You can find this example repo at [https://github.com/jordanpowell88/angular-counter](https://github.com/jordanpowell88/angular-counter)_

## Conclusion
**TLDR:** Angular is on 🔥! Signals, Standalone, SSR and so much more. Though the impacts of Signals is yet to be known as of this writing, I think that the safest way forward is to use high quality UI tests.

Admittedly I am biased, but I believe that [Cypress Component Tests](https://docs.cypress.io/guides/component-testing/angular/overview) are the best tool for this job. They are simpler to write and they encourage you to write the UI tests we talked about in this article. In the end, which tool you use is up to you and your team. The most important thing is that you feel confident that the side effects caused by refactoring to `signals` are caught before your users do!