Angular IDPStrong Junior
Модулі / Юніт-тести / Тест пайпа й guard
🔥 0

Модуль 8 · урок 4 з 9

Тест пайпа й guard

Два з пʼяти тестів, які просить лід. Обидва прості — тому з них і варто починати: перший зелений тест знімає психологічний барʼєр краще за будь-яку теорію.

Тест пайпа

// due-in.pipe.ts
@Pipe({ name: 'dueIn' })
export class DueInPipe implements PipeTransform {
  transform(value: string | Date | null, now: Date = new Date()): string {
    if (!value) return 'без терміну';

    const due = new Date(value);
    const days = Math.round((due.getTime() - now.getTime()) / 86_400_000);

    if (days < 0)   return `прострочено на ${Math.abs(days)} дн.`;
    if (days === 0) return 'сьогодні';
    if (days === 1) return 'завтра';
    return `через ${days} дн.`;
  }
}
// due-in.pipe.spec.ts
describe('DueInPipe', () => {
  const pipe = new DueInPipe();                 // жодного TestBed
  const now = new Date('2026-09-09T12:00:00');

  it('повертає підказку для порожнього значення', () => {
    expect(pipe.transform(null, now)).toBe('без терміну');
  });

  it('розпізнає сьогодні', () => {
    expect(pipe.transform('2026-09-09T18:00:00', now)).toBe('сьогодні');
  });

  it('розпізнає завтра', () => {
    expect(pipe.transform('2026-09-10T12:00:00', now)).toBe('завтра');
  });

  it('рахує прострочення', () => {
    expect(pipe.transform('2026-09-04T12:00:00', now)).toBe('прострочено на 5 дн.');
  });
});
Другий параметр now — не примха

Якби пайп брав new Date() усередині, тест довелось би або привʼязувати до поточної дати (і він зламався б завтра), або підміняти системний час. Параметр зі значенням за замовчуванням робить функцію передбачуваною й не ускладнює використання: у шаблоні пишеться так само {{ date | dueIn }}.

Це загальне правило: усе, що приходить ззовні — час, випадковість, поточний користувач — краще передавати параметром. Тоді тест стає тривіальним.

Тест guard: спершу чиста функція

// логіка окремо — рівно та ката з модуля 2
export function decideAccess(user: User | null, required?: string): true | string {
  if (!user) return '/login';
  if (!required) return true;
  if (user.roles.includes('admin')) return true;
  return user.roles.includes(required) ? true : '/forbidden';
}
describe('decideAccess', () => {
  it('неавторизованого веде на логін', () => {
    expect(decideAccess(null, 'producer')).toBe('/login');
  });

  it('пускає, коли роль не потрібна', () => {
    expect(decideAccess({ roles: [] }, undefined)).toBe(true);
  });

  it('пускає з потрібною роллю', () => {
    expect(decideAccess({ roles: ['producer'] }, 'producer')).toBe(true);
  });

  it('не пускає без ролі', () => {
    expect(decideAccess({ roles: ['artist'] }, 'producer')).toBe('/forbidden');
  });

  it('admin проходить будь-куди', () => {
    expect(decideAccess({ roles: ['admin'] }, 'producer')).toBe(true);
  });
});

Пʼять тестів, жодного Angular, виконуються миттєво. Саме так виглядає користь від того, що в модулі 2 логіка була винесена з guard у чисту функцію.

Тест самого guard

describe('roleGuard', () => {
  let auth: jasmine.SpyObj<AuthService>;
  let router: jasmine.SpyObj<Router>;

  beforeEach(() => {
    auth = jasmine.createSpyObj<AuthService>('AuthService', ['roles']);
    router = jasmine.createSpyObj<Router>('Router', ['createUrlTree']);

    TestBed.configureTestingModule({
      providers: [
        { provide: AuthService, useValue: auth },
        { provide: Router, useValue: router },
      ],
    });
  });

  function run(route: { data: Record<string, unknown> }) {
    // функціональний guard треба викликати в контексті інʼєкції
    return TestBed.runInInjectionContext(() =>
      roleGuard(route as ActivatedRouteSnapshot, {} as RouterStateSnapshot),
    );
  }

  it('пускає користувача з потрібною роллю', () => {
    auth.roles.and.returnValue(['producer']);

    expect(run({ data: { role: 'producer' } })).toBe(true);
  });

  it('редіректить без ролі', () => {
    auth.roles.and.returnValue(['artist']);
    router.createUrlTree.and.returnValue({} as UrlTree);

    run({ data: { role: 'producer' } });

    expect(router.createUrlTree).toHaveBeenCalledWith(['/forbidden']);
  });
});
runInInjectionContext

Функціональний guard усередині викликає inject(), а той працює лише в контексті інʼєкції. У тесті цей контекст створюється саме так. Без нього отримаєш помилку «inject() must be called from an injection context» — і це найчастіша перешкода при першому тесті guard.

Що варто перевіряти
  • Межі: порожнє значення, нуль, вчора й завтра.
  • Кожну гілку умови окремим тестом.
  • Що редірект іде саме туди, куди треба.
Чого не варто
  • Перевіряти формат рядка, який завтра змінить дизайнер.
  • Дублювати той самий сценарій з різними числами.
  • Тестувати сам Angular: що inject працює.

≈ 40 хв · +25 XP за урок