Hone logo
Hone
Problems

Angular Service Testing: Mocking HTTP Calls

This challenge focuses on a fundamental aspect of Angular development: testing services that interact with backend APIs. You will learn how to write effective unit tests for an Angular service by mocking its HTTP dependencies, ensuring your service logic can be tested in isolation without relying on a live server. This is crucial for creating robust and maintainable applications.

Problem Description

Your task is to write unit tests for an Angular service called ProductService. This service is responsible for fetching a list of products from a remote API. The ProductService uses Angular's HttpClient to make the actual HTTP request. In your tests, you will need to mock HttpClient to simulate responses from the API and verify that your ProductService correctly handles these responses.

Key Requirements:

  1. Create a ProductService: This service will have a method getProducts() that returns an observable of an array of Product objects.
  2. Simulate HTTP Calls: Use Angular's HttpClientTestingModule and HttpTestingController to mock HttpClient and control the responses to HTTP requests.
  3. Test getProducts(): Write a test case that verifies getProducts() makes the correct HTTP GET request to the expected URL.
  4. Test Successful Response: Write a test case that verifies getProducts() correctly processes a successful HTTP response and returns the expected data.
  5. Test Error Handling: Write a test case that verifies getProducts() correctly handles an HTTP error response.

Expected Behavior:

  • When getProducts() is called, it should make an HTTP GET request to /api/products.
  • If the HTTP request is successful and returns an array of products, getProducts() should emit that array.
  • If the HTTP request fails (e.g., returns a 404 or 500 error), getProducts() should propagate the error.

Edge Cases:

  • What happens if the API returns an empty array?
  • What happens if the API returns malformed data (though for this challenge, assume valid JSON for successful responses)?

Examples

Let's define a simple Product interface for clarity:

export interface Product {
  id: number;
  name: string;
  price: number;
}

Example 1: Testing the HTTP Request

// In your test file:
it('should make a GET request to the correct URL', () => {
  const mockProducts: Product[] = [
    { id: 1, name: 'Laptop', price: 1200 },
    { id: 2, name: 'Mouse', price: 25 }
  ];

  productService.getProducts().subscribe();

  // Expect a request to /api/products with GET method
  const req = httpTestingController.expectOne('/api/products');
  expect(req.request.method).toEqual('GET');

  // You might flush the response here in another test case
  req.flush(mockProducts); // This is here just to prevent the test from failing due to an unhandled request
});

Explanation: This test ensures that when getProducts() is called, it initiates an HTTP GET request to the specified endpoint /api/products.

Example 2: Testing Successful Data Retrieval

// In your test file:
it('should return the list of products on successful HTTP response', () => {
  const mockProducts: Product[] = [
    { id: 1, name: 'Laptop', price: 1200 },
    { id: 2, name: 'Mouse', price: 25 }
  ];

  productService.getProducts().subscribe((products) => {
    expect(products.length).toBe(2);
    expect(products).toEqual(mockProducts);
  });

  // Find the pending request and respond with mock data
  const req = httpTestingController.expectOne('/api/products');
  req.flush(mockProducts);
});

Explanation: This test verifies that if the HTTP request to /api/products returns a successful response with a JSON array of products, the getProducts() observable emits this array correctly.

Example 3: Testing Error Handling

// In your test file:
it('should handle HTTP errors', () => {
  const errorMessage = 'Server error';

  productService.getProducts().subscribe(
    () => fail('should have failed with an error'), // This should not be called
    (error) => {
      expect(error.message).toContain(errorMessage);
    }
  );

  const req = httpTestingController.expectOne('/api/products');
  // Simulate an error response
  req.error(new ErrorEvent('error', { message: errorMessage }));
});

Explanation: This test ensures that if the HTTP request to /api/products results in an error, the getProducts() observable correctly emits an error that can be caught by subscribers.

Constraints

  • The ProductService should be a standard Angular service.
  • Use HttpClientTestingModule and HttpTestingController for mocking HTTP requests.
  • All tests should be written in TypeScript within an Angular testing environment (e.g., using Jasmine or Jest).
  • The getProducts method should return an Observable<Product[]>.
  • The base URL for product fetching is hardcoded as /api/products within the service for simplicity.

Notes

  • You'll need to set up a basic Angular project structure for this challenge.
  • Remember to import HttpClientTestingModule in your TestBed.configureTestingModule.
  • The HttpTestingController allows you to control mock responses and assert that requests were made.
  • Use .flush() to simulate a successful response and .error() to simulate an error response.
  • Consider what happens if the service is called multiple times – does it make multiple requests? (This is beyond the scope of this basic challenge but a good thought exercise).
  • When mocking, think about the data shape expected by the service and returned by the "API."
Loading editor...
typescript