Angular登录组件单元测试报错:NullInjectorError及subscribe未定义
Hey there, let's break down and fix those issues you're facing with your Angular LoginComponent tests.
1. Fixing the NullInjectorError: No provider for LocationStrategy
The root cause here is that you're providing a custom Router spy alongside RouterTestingModule, which creates a conflict. RouterTestingModule already sets up all necessary routing dependencies (including LocationStrategy) for testing, so adding your own Router provider overrides that setup and breaks the dependency chain.
Solution:
Remove the custom Router provider from your TestBed configuration. If you need to spy on Router methods (like navigate), you can inject the Router instance from TestBed and spy on it directly instead.
2. Fixing the TypeError: Cannot read property 'subscribe' of undefined
This error happens because your AuthenticationService.login() method returns undefined in the test environment. In your component's onSubmit method, you call .subscribe() on the result of login()—but if that method doesn't return an Observable, you'll get this error.
Solution:
Mock the AuthenticationService properly so that its login method returns a valid Observable. Use Jasmine's createSpyObj to create a mock service with a spy for the login method, and make it return an Observable using of() from RxJS.
3. Bonus: Fix Duplicate @Component Decorator in Your Component
I noticed you have two @Component decorators on your LoginComponent—that's invalid Angular syntax! Merge them into a single decorator to avoid unexpected behavior.
Revised LoginComponent.ts (fixed decorator):
import { ActivatedRoute, Router } from '@angular/router'; import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { AuthenticationService } from '@/_services'; import { first } from 'rxjs/operators'; import { AuthentService } from '@/shared/_services/authentication/authentication.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.scss'] }) export class LoginComponent implements OnInit { loginForm: FormGroup; loading = false; submitted = false; returnUrl: string; error = ''; constructor( private formBuilder: FormBuilder, private route: ActivatedRoute, private router: Router, private authent: AuthentService, private authenticationService: AuthenticationService ) { if (this.authenticationService.currentUserValue) {} } ngOnInit() { this.loginForm = this.formBuilder.group({ email: ['', [Validators.required, Validators.email]], password: ['', Validators.required] }); } get f() { return this.loginForm.controls; } onSubmit() { this.submitted = true; if (this.loginForm.invalid) { return; } const data = Object.assign({}, this.loginForm.value); this.authenticationService.login(data) .pipe(first()) .subscribe(data => { this.authenticationService.redirectPage(); }); } }
Final Revised LoginComponent.spec.ts
Here's the updated test file with all fixes applied:
import { ActivatedRoute, Router } from '@angular/router'; import { ComponentFixture, TestBed, async } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { AuthentService } from '@/shared/_services/authentication/authentication.service'; import { AuthenticationService } from './../../_services/authentication.service'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { LoginComponent } from './login.component'; import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '../../shared/shared.module'; import { of } from 'rxjs'; describe('LoginComponent', () => { let component: LoginComponent; let fixture: ComponentFixture<LoginComponent>; let authenticationService: jasmine.SpyObj<AuthenticationService>; let router: jasmine.SpyObj<Router>; const fakeActivatedRoute = { snapshot: { queryParams: { returnUrl: '/' } } }; beforeEach(async(() => { // Create mock services const authServiceSpy = jasmine.createSpyObj('AuthenticationService', ['login', 'redirectPage']); // Mock login to return an Observable authServiceSpy.login.and.returnValue(of({})); await TestBed.configureTestingModule({ imports: [ ReactiveFormsModule, FormsModule, HttpClientTestingModule, SharedModule, RouterTestingModule.withRoutes([]) // Add empty routes to avoid warnings ], declarations: [LoginComponent], providers: [ { provide: ActivatedRoute, useValue: fakeActivatedRoute }, { provide: AuthenticationService, useValue: authServiceSpy }, AuthentService // Mock this too if it has dependencies! ] }).compileComponents(); // Get instances of mocked services for testing authenticationService = TestBed.inject(AuthenticationService) as jasmine.SpyObj<AuthenticationService>; router = TestBed.inject(Router) as jasmine.SpyObj<Router>; // Spy on router.navigate if you need to test navigation spyOn(router, 'navigate'); })); beforeEach(() => { fixture = TestBed.createComponent(LoginComponent); component = fixture.componentInstance; component.ngOnInit(); fixture.detectChanges(); }); it('form invalid when empty', () => { expect(component.loginForm.valid).toBe(false); }); it('username field validity', () => { const email = component.loginForm.controls.email; expect(email.valid).toBe(false); email.setValue(''); expect(email.hasError('required')).toBeTruthy(); email.setValue('invalid-email'); expect(email.hasError('email')).toBeTruthy(); email.setValue('test@example.com'); expect(email.valid).toBeTruthy(); }); it('password field validity', () => { const password = component.loginForm.controls.password; expect(password.valid).toBeFalsy(); password.setValue(''); expect(password.hasError('required')).toBeTruthy(); password.setValue('password123'); expect(password.valid).toBeTruthy(); }); it('should call authenticationService.login on submit', () => { component.loginForm.setValue({ email: 'test@example.com', password: 'password123' }); component.onSubmit(); expect(authenticationService.login).toHaveBeenCalledWith({ email: 'test@example.com', password: 'password123' }); expect(authenticationService.redirectPage).toHaveBeenCalled(); }); });
Key Changes in the Test File:
- Removed the conflicting
Routerprovider, usingRouterTestingModule.withRoutes([])instead (empty routes avoid console warnings) - Created a mock
AuthenticationServicewithcreateSpyObj, and madeloginreturn an Observable usingof({}) - Injected mocked services after TestBed configuration to use them in test cases
- Added a test to verify
loginandredirectPageare called on form submit
Additional Notes:
- If
AuthentServicehas its own dependencies (like HttpClient), mock it similarly toAuthenticationService - Ensure all service dependencies are properly provided or mocked in the test module to avoid further injector errors
内容的提问来源于stack exchange,提问作者Avinash

