Skip to content

Latest commit

 

History

History
764 lines (573 loc) · 28 KB

File metadata and controls

764 lines (573 loc) · 28 KB

Unit testing in Angular

  • Unit testing in angular(or any other language) is for,
    • Detecting bugs early
    • Code Quality and Maintainability
    • Refactoring and Code Changes
    • Ensuring the application behaves as expected in a constantly changing codebase.

Unit, Integration Testing and e2e testing?

  1. Unit testing

    • We perform testing for a single unit(Class).
  2. Integration Testing:

    • We perform testing for a multiple associated units (component, template, services, module) all together.
  3. e2e Testing:

    • We perform end to end testing to ensure all the functionality are working as expected. mainly this is a part of automation testing (protractor, cypress)

Jasmine:

  • It is programming language which is used to write test cases.

Karma:

  • It is test runner which is used to run all those test cases in http server.
  • It provides a test environment where you can run tests in real browsers

Both already being installed by angular cli at the time of installation.

The combination of Jasmine and Karma is a powerful setup for testing Angular applications, covering both unit tests and end-to-end (E2E) tests.


Explain basic components (describe, it, expect) of testing?

In Angular, testing is an essential aspect of software development to ensure that the application works as expected and to catch potential bugs or issues early in the development process.

Angular provides a robust testing framework that includes several key components. Let's explore the basic components of testing in Angular:

  1. describe:

    • The describe function is a part of a testing framework, such as Jasmine or Jest, that is commonly used with Angular.
    • It is often used to define a test suite.
    • It is used to group related test cases together, providing a way to organize and structure the tests.
    • The describe block typically includes a string describing the functionality being tested and a function that contains the individual test cases.
    describe('MyComponent', () => {
      // Test cases go here
    });
  2. it:

    • The it function is used to define an individual test case or specification within a describe block.
    • Each it block represents a specific behavior or expectation of a part of your code.
    • It contains a string that describes the specific test case and a function that contains the test logic.
    it('should do something', () => {
      it('should have a title', () => {
         // Test logic and expectations go here
     });
    
     it('should handle user input correctly', () => {
          // Test logic and expectations go here
     });
    });
  3. expect:

    • The expect function is used to assert or define expectations in your test cases.
    • It is typically used in conjunction with matcher functions to check whether a certain condition is met.
    • Matchers include functions like toEqual, toBe, toBeTruthy, etc., depending on the type of assertion you want to make.
    it('should add two numbers correctly', () => {
      const result = add(2, 3);
      expect(result).toBe(5);
    });

    In the example above, the expect function is used with the toBe matcher to check if the result of adding 2 and 3 is equal to 5.


What is beforeEach and afterEach in angular test?

In Angular testing, beforeEach and afterEach are Jasmine functions that help you set up and tear down the testing environment before and after each test runs.

These functions are used to configure the testing module, create component instances, or perform any other setup/teardown activities needed for your tests.

Here's a brief explanation of beforeEach and afterEach:

  1. beforeEach:

    • Purpose: beforeEach is a Jasmine function that defines a setup block that is executed before each individual test case (each it block) within a describe block.
    • Usage in Angular Testing: In Angular tests, beforeEach is commonly used to set up the testing module, configure TestBed, create instances of components or services, and perform any other necessary pre-test setup.
    • Example:
      describe('MyComponent', () => {
        let component: MyComponent;
        let fixture: ComponentFixture<MyComponent>;
      
        beforeEach(() => {
          TestBed.configureTestingModule({
            declarations: [MyComponent],
          });
      
          fixture = TestBed.createComponent(MyComponent);
          component = fixture.componentInstance;
        });
      
        // Test cases go here
        it('should have a valid title', () => {
          // Test logic goes here
         });
      });
  2. afterEach:

    • Purpose: afterEach is a Jasmine function that defines a teardown block that is executed after each individual test case within a describe block.
    • Usage in Angular Testing: afterEach is commonly used in Angular testing to clean up resources, reset variables, or perform any necessary post-test activities.
    • Example:
      describe('MyComponent', () => {
        let component: MyComponent;
        let fixture: ComponentFixture<MyComponent>;
      
        beforeEach(() => {
          TestBed.configureTestingModule({
            declarations: [MyComponent],
          });
      
          fixture = TestBed.createComponent(MyComponent);
          component = fixture.componentInstance;
        });
      
        afterEach(() => {
          // Clean up resources or perform post-test activities
        });
      
        // Test cases go here
        it('should have a valid title', () => {
         // Test logic goes here
         });
      });

By using beforeEach and afterEach, you can ensure that each test starts with a clean and consistent environment, and any changes made during the test are properly cleaned up afterward. These functions contribute to the overall maintainability and reliability of your Angular tests.


Command to test code coverage

Code coverage is a metric used in software development to measure the percentage of code that is executed by a set of automated tests.

ng test --code-coverage

How to test component and service?

Testing components and services in Angular is an essential part of ensuring the reliability and correctness of your application.

Angular provides a testing framework based on tools like Jasmine and Karma, and the Angular Testing utilities simplify the process of writing and running tests.

Let's look at how you can test both components and services in Angular:

Testing Components:

  1. Setup:

    • Import necessary modules and components.
    • Create a TestBed configuration for the component.
    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { MyComponent } from './my.component';
    
    describe('MyComponent', () => {
      let component: MyComponent;
      let fixture: ComponentFixture<MyComponent>;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          declarations: [MyComponent],
        });
    
        fixture = TestBed.createComponent(MyComponent);
        component = fixture.componentInstance;
      });
    });
  2. Test DOM and Behavior:

    • Write tests to check the rendering of the component, interactions, and expected behavior.
    it('should display a welcome message', () => {
      component.message = 'Hello, Angular!';
      fixture.detectChanges();
      expect(fixture.nativeElement.querySelector('div').textContent).toContain('Hello, Angular!');
    });
  3. Asynchronous Operations:

    • If your component involves asynchronous operations, use the async function or fakeAsync to handle asynchronous code.
    it('should fetch data asynchronously', fakeAsync(() => {
      // perform asynchronous operation
      tick(); // advance time in fakeAsync
      fixture.detectChanges();
      // assert
    }));
  4. Testing Input and Output Properties:

    • Test how the component interacts with input and output properties.
    it('should emit an event when a button is clicked', () => {
      spyOn(component.buttonClick, 'emit');
      const button = fixture.nativeElement.querySelector('button');
      button.click();
      expect(component.buttonClick.emit).toHaveBeenCalled();
    });

Testing Services:

  1. Setup:

    • Import necessary modules and services.
    • Create a TestBed configuration for the service.
    import { TestBed } from '@angular/core/testing';
    import { MyService } from './my.service';
    
    describe('MyService', () => {
      let service: MyService;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [MyService],
        });
    
        service = TestBed.inject(MyService);
      });
    });
  2. Testing Methods:

    • Write tests to check the methods of the service.
    it('should add two numbers', () => {
      const result = service.add(2, 3);
      expect(result).toBe(5);
    });
  3. Mock Dependencies:

    • If the service has dependencies, consider using TestBed to provide mock versions of these dependencies.
    beforeEach(() => {
      TestBed.configureTestingModule({
        providers: [
          MyService,
          { provide: SomeDependency, useClass: MockSomeDependency },
        ],
      });
    
      service = TestBed.inject(MyService);
    });
  4. Testing HTTP Requests:

    • When testing services that make HTTP requests, use Angular's HttpClientTestingModule to mock HTTP responses.
    import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
    
    describe('MyService', () => {
      let service: MyService;
      let httpTestingController: HttpTestingController;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [MyService],
        });
    
        service = TestBed.inject(MyService);
        httpTestingController = TestBed.inject(HttpTestingController);
      });
    
      it('should get data from the server', () => {
        service.getData().subscribe(data => {
          expect(data).toEqual(mockData);
        });
    
        const req = httpTestingController.expectOne('/api/data');
        expect(req.request.method).toEqual('GET');
        req.flush(mockData);
      });
    });

These are basic examples, and the actual tests will depend on your application's specific requirements. Angular's testing utilities provide various features and helpers to facilitate testing, so be sure to explore the official Angular Testing documentation for more details and advanced testing scenarios.


What is TestBud?

TestBed is a utility in Angular that provides a testing module environment for configuring and creating instances of Angular components, services, and other constructs within the context of unit tests.

It is an integral part of Angular's testing infrastructure and is commonly used in conjunction with testing frameworks like Jasmine or Jest.

Here are the key features and purposes of TestBed in Angular testing:

  1. Testing Module Configuration:

    • TestBed is used to configure an Angular testing module, which is a special type of module designed for testing purposes. The testing module is a container for the components, services, and other dependencies that are part of a unit test.
    import { TestBed } from '@angular/core/testing';
    
    beforeEach(() => {
      TestBed.configureTestingModule({
        declarations: [MyComponent],
        providers: [MyService],
      });
    });
  2. Component Creation:

    • TestBed.createComponent() is used to create an instance of an Angular component within the testing module. The resulting ComponentFixture provides access to the component instance, its native elements, and various testing utilities.
    let fixture: ComponentFixture<MyComponent>;
    let component: MyComponent;
    
    beforeEach(() => {
      fixture = TestBed.createComponent(MyComponent);
      component = fixture.componentInstance;
    });
  3. Service Injection:

    • TestBed is responsible for configuring dependency injection for services and other providers within the testing module. It allows you to inject mock services or other dependencies for testing purposes.
    TestBed.configureTestingModule({
      providers: [{ provide: MyService, useClass: MockService }],
    });
  4. Change Detection:

    • TestBed provides the detectChanges() method on the ComponentFixture, which triggers change detection for the component. This is crucial for updating the view based on changes to the component's state.
    it('should update the view after changes', () => {
      component.someProperty = 'new value';
      fixture.detectChanges();
      // Assertions based on updated view
    });
  5. Module Reset:

    • TestBed allows you to reset the testing module, providing a clean slate for each test. This is particularly useful when you want to isolate tests and avoid unintended side effects from one test to another.
    afterEach(() => {
      TestBed.resetTestingModule();
    });

Overall, TestBed simplifies the process of setting up and configuring the testing environment for Angular components and services.

It provides a controlled and isolated space for unit tests, making it easier for developers to write effective and maintainable tests for their Angular applications.


What is ComponentFixture?

In Angular testing, ComponentFixture is a utility class provided by the Angular testing framework. It is used to create and interact with an instance of an Angular component within a testing environment.

The ComponentFixture class is an essential part of the TestBed infrastructure, which allows you to configure and create a testing module for your Angular components.

Here are the main reasons why ComponentFixture is used in Angular testing:

  1. Component Instance Access:

    • ComponentFixture provides access to the instance of the Angular component that you are testing.
    • This allows you to interact with the component's properties, methods, and other members during your tests.
  2. Change Detection:

    • Angular relies on change detection to update the DOM when the state of a component changes.
    • ComponentFixture provides methods like detectChanges() that trigger change detection for the component, ensuring that the template is updated with the latest data.
    it('should update the view when a property changes', () => {
      component.someProperty = 'new value';
      fixture.detectChanges();
      expect(fixture.nativeElement.querySelector('p').textContent).toContain('new value');
    });
  3. DOM Interaction:

    • ComponentFixture gives you access to the native elements in the DOM associated with your Angular component.
    • This allows you to perform assertions on the rendered DOM and simulate user interactions.
    it('should handle a button click', () => {
      const button = fixture.nativeElement.querySelector('button');
      button.click();
      expect(component.someMethod).toHaveBeenCalled();
    });
  4. Asynchronous Operations:

    • Angular components often involve asynchronous operations such as HTTP requests or timers.
    • ComponentFixture provides utilities for dealing with asynchronous code, such as the whenStable() method.
    it('should handle asynchronous operations', async () => {
      // Some asynchronous operation in the component
      await fixture.whenStable();
      // Assertions after the asynchronous operation is complete
    });
  5. Lifecycle Hook Triggers:

    • ComponentFixture provides methods like destroy() to simulate the destruction of a component.
    • This is useful for testing the behavior of components during their lifecycle.
    it('should clean up resources on component destruction', () => {
      spyOn(component, 'ngOnDestroy');
      fixture.destroy();
      expect(component.ngOnDestroy).toHaveBeenCalled();
    });

By providing these capabilities, ComponentFixture makes it easier to write comprehensive tests for Angular components, covering various aspects such as template rendering, user interactions, and lifecycle events. It plays a crucial role in the Angular testing framework, allowing developers to isolate and test components in a controlled environment.


What us SpyOn in angular?

spyOn is a function provided by the Jasmine testing framework, commonly used in Angular testing.

It allows you to create spies, which are mock or stub functions, to observe and control the behavior of functions or methods in your code during testing.

Here's how spyOn works and some common use cases:

Basic Syntax:

// Syntax: spyOn(object, methodName)
const spy = spyOn(object, 'methodName');
  • object: The object containing the method you want to spy on.
  • methodName: The name of the method you want to spy on.

Common Use Cases:

  1. Observing Calls:

    • Use spyOn to create a spy and then call the original method. You can later assert whether the method was called and with what arguments.
    const obj = {
      method: function(value) {
        // some logic
      }
    };
    
    const spy = spyOn(obj, 'method');
    obj.method('test');
    expect(spy).toHaveBeenCalled();
  2. Stubbing:

    • spyOn can be used to replace the original method with a spy that returns a predefined value or executes custom logic.
    const obj = {
      method: function() {
        // some logic
      }
    };
    
    const spy = spyOn(obj, 'method').and.returnValue('custom value');
    const result = obj.method();
    expect(result).toEqual('custom value');
  3. Observing Property Access:

    • You can also spy on property access using spyOn.
    const obj = {
      get property() {
        // some logic
      }
    };
    
    const spy = spyOnProperty(obj, 'property', 'get');
    const value = obj.property;
    expect(spy).toHaveBeenCalled();
  4. Spying on Angular Services:

    • In Angular testing, spyOn is often used to spy on methods of Angular services. This allows you to control the behavior of services during unit tests.
    // Angular TestBed configuration
    TestBed.configureTestingModule({
      providers: [MyService],
    });
    
    // Spying on a service method
    const myService = TestBed.inject(MyService);
    const spy = spyOn(myService, 'someMethod');

spyOn is a powerful tool for testing because it enables you to isolate the code under test, control its behavior, and assert that it interacts with other parts of the system as expected. It is widely used in conjunction with other Jasmine and Angular testing utilities to facilitate effective and expressive unit tests.


What are the different matcher in angular?

In Angular testing, matchers are functions provided by the Jasmine testing framework to perform various types of assertions in your tests.

These matchers help you express expectations about the behavior of your code. Here are some commonly used matchers in Angular testing:

  1. Equality Matchers:

    • expect(x).toBe(y): Checks if x and y are the same object. It uses strict equality (===) for comparison.
    • expect(x).toEqual(y): Deep equality check. It compares the values of x and y recursively.
  2. Truthiness Matchers:

    • expect(x).toBeTruthy(): Checks if x is truthy (i.e., not falsy).
    • expect(x).toBeFalsy(): Checks if x is falsy.
  3. Comparison Matchers:

    • expect(x).toBeGreaterThan(y): Checks if x is greater than y.
    • expect(x).toBeLessThan(y): Checks if x is less than y.
    • expect(x).toBeGreaterThanOrEqual(y): Checks if x is greater than or equal to y.
    • expect(x).toBeLessThanOrEqual(y): Checks if x is less than or equal to y.
  4. Type Matchers:

    • expect(x).toBeInstanceOf(Constructor): Checks if x is an instance of the specified constructor.
    • expect(x).toBeTypeOf('string'): Checks if the type of x is the specified type.
  5. String Matchers:

    • expect('hello').toMatch(/hello/): Checks if the string matches the specified regular expression.
    • expect('hello').toContain('ello'): Checks if the string contains the specified substring.
  6. Array Matchers:

    • expect([1, 2, 3]).toContain(2): Checks if the array contains the specified value.
    • expect([1, 2, 3]).toEqual([1, 2, 3]): Deep equality check for arrays.
  7. Object Matchers:

    • expect(obj).toHaveProperty('propertyName'): Checks if the object has the specified property.
    • expect(obj).toEqual(jasmine.objectContaining({ prop: 'value' })): Checks if the object matches the specified partial object.
  8. Function Matchers:

    • expect(fn).toThrow(): Checks if calling the function fn throws an exception.
  9. Spy Matchers:

    • expect(spy).toHaveBeenCalled(): Checks if a spy has been called.
    • expect(spy).toHaveBeenCalledTimes(n): Checks if a spy has been called exactly n times.
    • expect(spy).toHaveBeenCalledWith(arg1, arg2, ...): Checks if a spy has been called with the specified arguments.

These are just a few examples of the many matchers available in Jasmine for use in Angular testing. You can combine these matchers to create expressive and meaningful assertions in your tests, helping you ensure that your code behaves as expected.


How to test forkJoin code in angular?

In Angular, forkJoin is a RxJS operator used for combining multiple observable sequences into one observable sequence. Testing code that involves forkJoin typically requires using testing utilities provided by Angular and RxJS. Below is a guide on how to test code that includes forkJoin:

Let's assume you have a service that makes use of forkJoin. Here is an example service:

import { Injectable } from '@angular/core';
import { forkJoin, Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root',
})
export class DataService {
  constructor(private http: HttpClient) {}

  fetchData(): Observable<any[]> {
    const observable1 = this.http.get('api/data1');
    const observable2 = this.http.get('api/data2');
    const observable3 = this.http.get('api/data3');

    return forkJoin([observable1, observable2, observable3]);
  }
}

And here is how you can test it using Jasmine and Angular testing utilities:

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { DataService } from './data.service';

describe('DataService', () => {
  let service: DataService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [DataService],
    });
    service = TestBed.inject(DataService);
  });

  it('should fetch data using forkJoin', (done: DoneFn) => {
    service.fetchData().subscribe(
      (result) => {
        // Perform assertions on the result
        expect(result.length).toBe(3);
        // Add more specific assertions based on the expected data

        done(); // Call done() to signal that the asynchronous test is complete
      },
      (error) => {
        fail(error); // Fail the test if an error occurs
        done(); // Ensure that done() is called in case of an error
      }
    );
  });
});

Explanation:

  1. HttpClientTestingModule:

    • Use HttpClientTestingModule to mock the HttpClient service. This allows you to intercept HTTP requests and provide mock responses.
  2. Service Configuration:

    • Configure the testing module with the necessary imports and providers, including the DataService you want to test.
  3. Test Case:

    • Write a test case that subscribes to the observable returned by fetchData(). The test function takes a DoneFn parameter, which is used to signal the completion of the asynchronous test.
  4. Assertions:

    • Inside the subscription, perform the necessary assertions on the result of forkJoin. In this example, we're checking the length of the array returned by forkJoin.
  5. Error Handling:

    • Add error handling to the subscription. If an error occurs, fail the test and call done() to signal completion.
  6. Async Completion:

    • Call done() to indicate the completion of the asynchronous test. This is crucial for asynchronous tests to ensure that Jasmine waits for the test to finish.

This example assumes that your service uses Angular's HttpClient for making HTTP requests. Adjust the test case based on your specific service implementation and the nature of the data you are fetching with forkJoin.

-or-----------

When you're working with the forkJoin operator in Angular, which is used to combine multiple observables and emit their last values as an array, you'll want to test it to ensure that your code behaves as expected. Here's a guide on how you can test code that involves forkJoin:

Let's assume you have a service method that uses forkJoin to combine multiple HTTP requests:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { forkJoin, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {

  constructor(private http: HttpClient) { }

  fetchData(): Observable<any[]> {
    const request1 = this.http.get('/api/data1');
    const request2 = this.http.get('/api/data2');

    return forkJoin([request1, request2]);
  }
}

Now, let's create tests for this code using Jasmine and Angular testing utilities:

  1. Mock the HttpClient:

    • Use Angular's HttpClientTestingModule to mock the HttpClient for testing.
    import { TestBed } from '@angular/core/testing';
    import { HttpClientTestingModule } from '@angular/common/http/testing';
    import { DataService } from './data.service';
    
    describe('DataService', () => {
      let service: DataService;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [DataService]
        });
    
        service = TestBed.inject(DataService);
      });
    
      // Test cases go here
    });
  2. Mock the HTTP Requests:

    • Use HttpClientTestingModule to provide mock responses for the HTTP requests.
    import { TestBed } from '@angular/core/testing';
    import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
    import { DataService } from './data.service';
    
    describe('DataService', () => {
      let service: DataService;
      let httpTestingController: HttpTestingController;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [DataService]
        });
    
        service = TestBed.inject(DataService);
        httpTestingController = TestBed.inject(HttpTestingController);
      });
    
      afterEach(() => {
        httpTestingController.verify();
      });
    
      // Test cases go here
    });
  3. Test forkJoin Behavior:

    • Write test cases to ensure that forkJoin is behaving as expected.
    it('should fetch data using forkJoin', () => {
      const mockData1 = { key1: 'value1' };
      const mockData2 = { key2: 'value2' };
    
      service.fetchData().subscribe(data => {
        expect(data[0]).toEqual(mockData1);
        expect(data[1]).toEqual(mockData2);
      });
    
      const req1 = httpTestingController.expectOne('/api/data1');
      const req2 = httpTestingController.expectOne('/api/data2');
    
      req1.flush(mockData1);
      req2.flush(mockData2);
    });

    This test case checks that the fetchData method uses forkJoin to combine the results of two HTTP requests and emits the combined data as an array.

Make sure to adjust the URLs and data in the test case according to your actual API endpoints and expected responses. Additionally, add more test cases to cover different scenarios, such as error handling or testing with different types of observables in the forkJoin array.