#Testing Angular Signals? Can't find any docs

22 messages · Page 1 of 1 (latest)

slim stratus
#

Hi guys, so I'm trying to write tests with Angular signals and can't find any examples in internet.
If I mock todosSig with Jest then obviously it's not a signal anymore and computed is not working on it.
So noTodosClass doesn't update it's value.
Another way is to create signals inside test and check them but I'm not sure if it's a good idea.
Any links to source code with testing signals is appreciated.

// todos.service.ts
export class TodosService {
  todosSig = signal<TodoInterface[]>([]);
}
// footer.component.ts
export class FooterComponent {
  todosService = inject(TodosService);
  noTodosClass = computed(() => this.todosService.todosSig().length === 0);
}
// footer.component.spec.ts
describe('FooterComponent', () => {
  let component: FooterComponent;
  let fixture: ComponentFixture<FooterComponent>;
  const todosServiceMock = {
    todosSig: jest.fn().mockReturnValue([])
  };

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [FooterComponent],
      providers: [{ provide: TodosService, useValue: todosServiceMock }],
    }).compileComponents();

    fixture = TestBed.createComponent(FooterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should be visible with todos', () => {
    todosServiceMock.todosSig.mockReturnValue([
      { id: 1, text: 'Sample Todo', isCompleted: false },
    ]);
    fixture.detectChanges();
    const footer = fixture.debugElement.query(
      By.css('[data-testid="footer"]')
    );
    expect(footer.classes['hidden']).not.toBeDefined();
  });
})

mortal ridge
#

Usually when I'm testing a component or anything else that has an observable as property I don't do jest.fn() but rather of() to mock the property. Therefore I would do the same and don't restrict myself from creating a signal for testing. Can't wait to see what JB is writing because this is kinda new to me x)

eager shore
#

There's no reason to mock a signal. Just use a real signal in your mock service. And since your service doesn't do anything other than having a signal, mocking it doesn't really add anything.

// todos.service.ts
export class TodosService {
  todosSig = signal<TodoInterface[]>([]);
}
// footer.component.ts
export class FooterComponent {
  todosService = inject(TodosService);
  noTodosClass = computed(() => this.todosService.todosSig().length === 0);
}
// footer.component.spec.ts
describe('FooterComponent', () => {
  let component: FooterComponent;
  let fixture: ComponentFixture<FooterComponent>;
 
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [FooterComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(FooterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should be visible with todos', () => {
    TestBed.inject(TodosService).todosSig.set([
      { id: 1, text: 'Sample Todo', isCompleted: false },
    ]);
    fixture.detectChanges();
    const footer = fixture.debugElement.query(
      By.css('[data-testid="footer"]')
    );
    expect(footer.classes['hidden']).not.toBeDefined();
  });
})
slim stratus
eager shore
#

A signal is a reactive value. You don't mock values, you mock behaviors. Imagine if your service exposed an array instead of a Signal<array>. You wouldn't create a mock array. Same here.
You can create a fake service if you want (since that's the dependency of your component). The fake service, however, will look quite the same as the real one in this case, since your service doesn't do anything.

slim stratus
whole root
#

@slim stratus this is unrelated to your issue, but I'm curious: how do you bind the class based on the signals value?

mortal ridge
#

using ngClass directive probably

slim stratus
#

@whole root

<footer
  class="footer"
  data-testid="footer"
  [ngClass]="{ hidden: noTodosClass() }"
>

eager shore
#

Or class binding:
[class.hidden]="noTodosClass()"

whole root
#

Ah alright. I thought it was a class on the host. Thanks for the quick response though

eager shore
whole root
eager shore
#
@HostBinding('[class.hidden]')
get hidden() {
  return this.noTodosClass();
}
#

Angular discourages using HostBinding now, though.

whole root
#

yeah using a getter is like the last resort.. well I've been exploring host bindings with signals a little over the past weeks and ended up writing a library, to make it easier. There you can just write noTodosClass = useClass('hidden') which will return a Signal<boolean> or use bindClass('hidden', mySignal) to bind an existing signal

#

I didn't know HostBinding is discourageed now, can you point me to some more info about that?

eager shore
#
GitHub

PR Checklist
Please check if your PR fulfills the following requirements:

The commit message follows our guidelines: https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit
Tests for...

The web development framework for building modern apps.

whole root
#

It's kinda weird to me still. And I'm not sure the host property is a better solution in terms of dev-experience

eager shore
#

What I dislike about it is that it's string-based, and in the decorator, i.e. not where you look for functional stuff in a component. What I like about it though is that the syntax is almost identical to the syntax you would use in a template:
[class.foo]="bar" -> '[class.foo]: 'bar'
(click)="doSomething($event)" -> '(click)': 'doSomething($event)'