Testing Angular Material Basic Spreadsheet

Previously, you had an idea to move a spreadsheet from an email trail onto a website. To achieve that, you used an Angular Material Basic Spreadsheet. You showed it to your manager and he thought it had potential if you add a few more features. Before going ahead with that, you realise that, to achieve complexity scale, you will have to start writing some tests.

As a reminder, the key elements of the Angular Material Basic Spreadsheet are the component that displays individual orders. This component is used by an Angular Material table based table component that maps a number of orders onto the screen similar to a spreadsheet based on the date and product of the order, as shown below.

The component to be tested.

Testing Individual Order Component

The following is the code and template behind the individual order component.
order.component.ts
order.component.html

// order.component.ts
import { Component, OnInit, Input } from '@angular/core';

import { Order } from './order.model';

@Component({
  selector: 'app-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.css']
})
export class OrderComponent implements OnInit {
  @Input() order: Order;

  constructor() { }

  ngOnInit() {
  }

}
<!-- order.component.html -->
<ng-container *ngIf="order">{{ order.value | number }}</ng-container>

To figure out the tests that need to be written, let’s think about what could go wrong. Firstly, we could have forgotten to import something or setup something incorrectly. Next, we could have assumed that anyone that uses the component will always pass in an order. Thirdly, when an order is passed in, we could have forgotten to use the number pipe to display the value. The following specification tests for those three cases.
order.component.spec.ts

// order.component.spec.ts
import { TestBed, ComponentFixture } from '@angular/core/testing';

import { OrderComponent } from './order.component';
import { DebugElement } from '@angular/core';

describe('OrderComponent', () => {
  let fixture: ComponentFixture<OrderComponent>;
  let component: OrderComponent;
  let debugElement: DebugElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [OrderComponent]
    });

    fixture = TestBed.createComponent(OrderComponent);
    component = fixture.componentInstance;
    debugElement = fixture.debugElement;
  });

  it('should create', () => {
    fixture.detectChanges();
    expect(component).toBeTruthy();
  });

  describe('order null', () => {
    it('should display empty template.', () => {
      // Setting order to null
      component.order = null;

      // Triggering change detection
      fixture.detectChanges();

      // Checking template
      expect(debugElement.nativeElement.textContent).toEqual('');
    });
  });

  describe('order not null', () => {
    it('should display order value formatted as number.', () => {
      // Setting order to null
      component.order = {date: '2000-01-01', product: 'product 1', value: 1000};

      // Triggering change detection
      fixture.detectChanges();

      // Checking template
      expect(debugElement.nativeElement.textContent).toEqual('1,000');
    });
  });
});

On lines 8-20 the standard setup for component tests is defined. The test on lines 22-25 checks that the component is created. The test on lines 27-38 checks what happens if the input Order is null. Finally, the test on lines 40-51 checks that the Order value is displayed using the number pipe.

Here the DebugElement is used to inspect the template. Since the template is relatively simple, we only ever check the text content of the whole template under the different test cases.

Testing Table Component

The following is the code and template for the order table component.
order-table.component.ts
order-table.component.html

// order-table.component.ts
import { Component, OnInit } from '@angular/core';

import { OrderTable } from './order-table.model';
import { OrderService } from './order.service';

@Component({
  selector: 'app-order-table',
  templateUrl: './order-table.component.html',
  styleUrls: ['./order-table.component.css']
})
export class OrderTableComponent implements OnInit {
  orderTable: OrderTable;
  productHeader = 'Product';
  headers: string[];

  constructor(private orderService: OrderService) { }

  ngOnInit() {
    this.orderTable = this.orderService.getOrderTable();
    this.headers = [this.productHeader].concat(this.orderTable.headers);
  }

}
<!-- order-table.component.html -->
<ng-container *ngIf="orderTable.rowLabels.length">
  <table mat-table [dataSource]="orderTable.rowLabels" class="mat-elevation-z8">
    <ng-container [matColumnDef]="header" *ngFor="let header of headers">
      <th mat-header-cell *matHeaderCellDef>
        <ng-container *ngIf="header === productHeader">{{ header }}</ng-container>
        <ng-container *ngIf="header !== productHeader">{{ header | date }}</ng-container>
      </th>
      <td mat-cell *matCellDef="let element">
        <ng-container *ngIf="header === productHeader">{{ element }}</ng-container>
        <ng-container *ngIf="header !== productHeader"><app-order [order]="orderTable.orderMap[header][element]"></app-order></ng-container>
      </td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="headers"></tr>
    <tr mat-row *matRowDef="let row; columns: headers;"></tr>
  </table>
</ng-container>

The table component is more complex and will require more tests. Broadly, there are tests for the component class and for the template. The main test for the component class is that the getOrderTable function is called on initialisation. Ideally, component code is tested without the use of the TestBed as it tends to slow down tests. However, since the component code to be tested is linked to an Angular event (OnInit), in this case it is better to use the TestBed and, therefore, the standard Angular lifecycle functions. The template tests are complex due to the number of ngIf statements and other logic in the template. Additionally, there are some complexities testing Angular Material tables. The following is the test code that checks common cases.
order-table.component.spec.ts

// order-table.component.spec.ts
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { MatTableModule } from '@angular/material';

import { OrderTableComponent } from './order-table.component';
import { Order } from './order.model';
import { OrderService } from './order.service';

@Component({
  selector: 'app-order',
  template: `
    {{ order.date }}
    {{ order.product }}
    {{ order.value }}
  `
})
class TestOrderComponent {
  @Input() order: Order;
}

describe('OrderTableComponent', () => {
  let fixture: ComponentFixture<OrderTableComponent>;
  let component: OrderTableComponent;
  let nativeElement: HTMLElement;
  let orderServiceSpy: jasmine.SpyObj<OrderService>;

  beforeEach(() => {
    // Creating mock OrderService
    orderServiceSpy = jasmine.createSpyObj('OrderService', ['getOrderTable']);

    TestBed.configureTestingModule({
      declarations: [OrderTableComponent, TestOrderComponent],
      providers: [{provide: OrderService, useValue: orderServiceSpy}],
      imports: [MatTableModule]
    });

    fixture = TestBed.createComponent(OrderTableComponent);
    component = fixture.componentInstance;
    nativeElement = fixture.nativeElement;
  });

  describe('TestOrderComponent', () => {
    it('should create', () => {
      // Defining getOrderTable return value
      orderServiceSpy.getOrderTable.and.returnValue({
        headers: [],
        rowLabels: [],
        orders: [],
        orderMap: {}
      });

      // Triggering change detection
      fixture.detectChanges();

      // Checking component create
      expect(component).toBeTruthy();
    });

    describe('construction', () => {
      it('should call getOrderTable on OrderService', () => {
        // Defining getOrderTable return value
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: [],
          rowLabels: [],
          orders: [],
          orderMap: {}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking getOrderTable call
        expect(orderServiceSpy.getOrderTable).toHaveBeenCalledWith();
      });
    });

    describe('empty table', () => {
      it('should display empty template.', () => {
        // Defining getOrderTable return value
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: [],
          rowLabels: [],
          orders: [],
          orderMap: {}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking template
        expect(nativeElement.textContent).toEqual('');
      });
    });

    describe('single item table', () => {
      it('should display Product and date header', () => {
        // Defining getOrderTable return value
        const order = {
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        };
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01'],
          rowLabels: ['product 1'],
          orders: [order],
          orderMap: {'2000-01-01': {'product 1': order}}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(2);
        const ths = trs.item(0).querySelectorAll('th');
        expect(ths.length).toEqual(2);
        ['Product', 'Jan 1, 2000'].forEach((expectedHeader, idx) => expect(ths.item(idx).innerText).toEqual(expectedHeader));
      });

      it('should display single row with Order', () => {
        // Defining getOrderTable return value
        const order = {
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        };
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01'],
          rowLabels: ['product 1'],
          orders: [order],
          orderMap: {'2000-01-01': {'product 1': order}}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(2);
        const tds = trs.item(1).querySelectorAll('td');
        expect(tds.length).toEqual(2);
        ['product 1', '2000-01-01 product 1 1'].forEach((expectedItem, idx) => expect(tds.item(idx).innerText).toEqual(expectedItem));
      });
    });

    describe('multiple item table with single row', () => {
      it('should display Product and date headers', () => {
        // Defining getOrderTable return value
        const orders = [{
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        }, {
          date: '2000-01-02',
          product: 'product 2',
          value: 2
        }];
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01', '2000-01-02'],
          rowLabels: ['product 1'],
          orders: orders,
          orderMap: {
            '2000-01-01': {'product 1': orders[0]},
            '2000-01-02': {'product 1': orders[1]}
          }
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(2);
        const ths = trs.item(0).querySelectorAll('th');
        expect(ths.length).toEqual(3);
        [
          'Product', 'Jan 1, 2000', 'Jan 2, 2000'
        ].forEach((expectedHeader, idx) => expect(ths.item(idx).innerText).toEqual(expectedHeader));
      });

      it('should display single row with Orders', () => {
        // Defining getOrderTable return value
        const orders = [{
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        }, {
          date: '2000-01-02',
          product: 'product 1',
          value: 2
        }];
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01', '2000-01-02'],
          rowLabels: ['product 1'],
          orders: orders,
          orderMap: {
            '2000-01-01': {'product 1': orders[0]},
            '2000-01-02': {'product 1': orders[1]}
          }
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(2);
        const ths = trs.item(1).querySelectorAll('td');
        expect(ths.length).toEqual(3);
        [
          'product 1', '2000-01-01 product 1 1', '2000-01-02 product 1 2'
        ].forEach((expectedItem, idx) => expect(ths.item(idx).innerText).toEqual(expectedItem));
      });
    });

    describe('multiple item table with single column', () => {
      it('should display Product and date header', () => {
        // Defining getOrderTable return value
        const orders = [{
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        }, {
          date: '2000-01-01',
          product: 'product 1',
          value: 2
        }];
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01'],
          rowLabels: ['product 1', 'product 2'],
          orders: orders,
          orderMap: {'2000-01-01': {
            'product 1': orders[0],
            'product 2': orders[1]
          }}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(3);
        const ths = trs.item(0).querySelectorAll('th');
        expect(ths.length).toEqual(2);
        ['Product', 'Jan 1, 2000'].forEach((expectedHeader, idx) => expect(ths.item(idx).innerText).toEqual(expectedHeader));
      });

      it('should display multiple rows with Orders', () => {
        // Defining getOrderTable return value
        const orders = [{
          date: '2000-01-01',
          product: 'product 1',
          value: 1
        }, {
          date: '2000-01-01',
          product: 'product 2',
          value: 2
        }];
        orderServiceSpy.getOrderTable.and.returnValue({
          headers: ['2000-01-01'],
          rowLabels: ['product 1', 'product 2'],
          orders: orders,
          orderMap: {'2000-01-01': {
            'product 1': orders[0],
            'product 2': orders[1]
          }}
        });

        // Triggering change detection
        fixture.detectChanges();

        // Checking headers in template
        const trs = nativeElement.querySelectorAll('tr');
        expect(trs.length).toEqual(3);
        let tds = trs.item(1).querySelectorAll('td');
        expect(tds.length).toEqual(2);
        ['product 1', '2000-01-01 product 1 1'].forEach((expectedItem, idx) => expect(tds.item(idx).innerText).toEqual(expectedItem));
        tds = trs.item(2).querySelectorAll('td');
        expect(tds.length).toEqual(2);
        ['product 2', '2000-01-01 product 2 2'].forEach((expectedItem, idx) => expect(tds.item(idx).innerText).toEqual(expectedItem));
      });
    });
  });
});

The test setup is done on lines 23-41. The tests on lines 43-76 check the code of the component. The tests on lines 78-286 check the template under a range of scenarios.

Test Setup for Angular Material Table

There are a few interesting pieces to highlight for the tests. As the OrderTemplateComponent has a dependency on the OrderComponent, it needs to be given to the TestBed along with the OrderTableComponent. For testing, it is better to define a mock implementation of the component rather than use the real component, which is done on lines 10-20.

At the time of writing, the DebugElement does not work with the Angular Material table. This means that, to figure out exactly which OrderComponent is displayed in a cell, we need to make use of the template. Each OrderComponent displays an Order. Therefore, it should be enough to display all the member variables of the Order in the template of the mock OrderComponent to know that the correct Order was passed.

Since the template of the OrderTableComponent makes use of the Angular Material table, we need to pass in the MatTableModule to the TestBed.

Component Code Tests

The tests on lines 43-76 check that the component can be constructed and that the getOrderTable function is called on the OrderService during component initialisation. In this case, the component create test is a little more complicated because the OrderService needs to be called during initialisation which means that the getOrderTable function must at least exist. Also, during the initialisation, the headers member variable is used on the returned value of getOrderTable, so we may as well return an empty OrderTable.

Component Template Tests

The main reason for the length of the tests are the definition of the OrderTable to display in each test. There is a trade off between pre-defining or repeatedly defining them in each test. In production code brevity is preferred, so it is often better to define a range of OrderTables and importing them. However, for tests, it is better to have a single test be as much of a complete story as is needed to understand the test. As what is displayed in the template is tightly linked to the OrderTable, there are significant advantages to explicitly stating the OrderTable in each test.

The test scenarios are an empty table, a single item table and tests where either multiple columns or rows are displayed. In each case, the headers and rows are checked.

With these tests you have made an important step towards being able to scale up the complexity of the website. Now it is time to work on additional features! The most important feature request your manager has made is to make the spreadsheet editable so that people can revise the orders that were placed over time.

Testing Reactive Components in Angular

The Service with a Subject pattern in Angular is a good way to get started with reactive programming (ie. making use of RxJS) in Angular. The following article describes how to get started with the pattern: Getting Started with Service with a Subject in Angular.

That pattern leads to writing reactive components for which standard Angular component testing would lead to more complicated tests. With some changes to the standard Angular component testing methodologies, the tests can be simplified significantly.

Reactive Component under Test

For reference, the following is the typescript and HTML template for the component.
name.component.ts

// name.component.ts
import { Component, OnInit } from '@angular/core';

import { NameService } from './name.service';

@Component({
  selector: 'app-name',
  templateUrl: './name.component.html',
  styleUrls: ['./name.component.css']
})
export class NameComponent implements OnInit {
  constructor(public nameService: NameService) { }

  ngOnInit() {
    this.nameService.loadName();
  }

}

name.component.html

<!-- name.component.html -->
<ng-container *ngIf="nameService.name$() | async as name">
  {{ name }}
</ng-container>

Since the component depends on a reactive service, for reference, the following is sample code for the reactive service. For testing the reactive service see Testing Reactive Service with a Subject in Angular.
name.service.ts

// name.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NameService {
  private mName$ = new BehaviorSubject<string>(null);

  name$(): Observable<string> {
    return this.mName$;
  }

  constructor(private httpClient: HttpClient) { }

  loadName() {
    this.httpClient.get<string>('name URL')
      .subscribe(
        name => this.mName$.next(name)
      );
  }
}

To figure out what needs to be tested, let’s think about what could go wrong. There are 3 main things that are likely to go wrong. The first is that the component doesn’t even get created due to some syntactical or import error. The second is that the loadName function on the NameService doesn’t get called during initialisation. The third is that, once the NameService name$ function emits a name, it doesn’t get displayed in the component.

Reactive Component Tests

The following is the test code that checks the 3 things that are likely to go wrong described earlier.
name.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';

import { NameService } from './name.service';
import { NameComponent } from './name.component';

describe('NameComponent', () => {
  let nameServiceSpy: jasmine.SpyObj<NameService>;
  let component: NameComponent;
  let fixture: ComponentFixture<NameComponent>;

  beforeEach(() => {
    nameServiceSpy = jasmine.createSpyObj('NameService', ['loadName', 'name$']);

    TestBed.configureTestingModule({
      declarations: [ NameComponent ],
      providers: [ { provide: NameService, useValue: nameServiceSpy } ]
    });

    fixture = TestBed.createComponent(NameComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    // Checking component creation
    expect(component).toBeTruthy();
  });

  it('should call loadName on NameService', () => {
    fixture.detectChanges();

    // Checking loadName call
    expect(nameServiceSpy.loadName).toHaveBeenCalledWith();
  });

  it('should display NameService name', () => {
    // Setting name in service
    nameServiceSpy.name$.and.returnValue(of('the name'));

    fixture.detectChanges();

    // Checking displayed name
    expect(fixture.debugElement.nativeElement.innerText).toEqual('the name');
  });
});

Test Setup

The setup common to all tests is on lines 8-21 which define the component under test and other supporting variables needed for he tests. Lines 13-21 define the standard TestBed initialisation and injection logic. The three test scenarios are defined on lines 24-27 (checking the component can be created), 29-34 (checking that loadName is called on NameService) and 36-44 (checking that the name emitted on name$ is displayed).

Component Setup

The test on lines 24-27 checks that the component can be setup. This is a simple test but is a very useful step for verifying that all the component plumbing is correct and that the test setup has actually worked. If this test fails it is likely because an injection is missing or because the TestBed was not setup correctly.

loadName Call

The test on lines 29-34 checks that the loadName function is called on the NameService. For the ngOnInit function to be triggered, change detection is run on line 30. Line 33 checks that loadName has been called on the NameService.

Name Display

The test on lines 36-44 checks that, when name$ on NameService emits a new name, that name is displayed in the component. Line 38 triggers emitting a new name on name$. Line 40 triggers change detection so that the name is displayed. Line 43 checks that the name is displayed in the component.

These tests are the base on which tests can be built if the logic in the component becomes more complicated. Examples of additional complications are having to pass arguments to the service loadName call, displaying data from multiple services or subjects and a more complicated template that includes conditional displaying of data.

Testing Decorated Python Functions

Decorators are a great way of adding functionality to a function with minimal impact on the function itself. On top of that, decorator logic can be re-used on other functions that also require the new functionality. For example the following decorator prints a message to standard output every time a function is called.

# plain_main.py
"""Demonstrates a simple decorator."""


def decorator(func):
    """
    A simple decorator that adds printing a message on a function call.

    Args:
        func: The function to decorate.

    Returns:
        The decorated function.
    """
    def inner(*args, **kwargs):
        """Function that is called instead of original function."""
        print('The decorator was called.')
        return func(*args, **kwargs)

    return inner


@decorator
def main():
    print('The main function was called.')


if __name__ == '__main__':
    print('Calling the main function.')
    main()
$ python3 plain_main.py 
Calling the main function.
The decorator was called.
The main function was called.

The drawback of decorators is that the decorator is applied as soon as the interpreter reaches the function definition and it is hard to access the original function without the decorator applied. This might be desirable during testing where testing of the function and the decorator should be separated.

Adding Testing Guard Logic to a Decorator

The solution is to optionally skip the decorator logic if a certain condition is met that is only true during testing. For example, skip the decorator logic if the TESTING environment variable is set.

# check_main.py
"""Demonstrates a decorator with a testing guard."""

import os


def decorator(func):
    """
    A simple decorator that adds printing a message on a function call unless
    the TESTING environment variable is set.

    Args:
        func: The function to decorate.

    Returns:
        The decorated function.
    """
    def inner(*args, **kwargs):
        """Function that is called instead of original function."""
        # Checking for TESTING environment variable
        if os.getenv('TESTING') is not None:
            # Skipping decortor logic
            return func(*args, **kwargs)

        # Running decorator logic
        print('The decorator was called.')
        return func(*args, **kwargs)

    return inner


@decorator
def main():
    print('The main function was called.')


if __name__ == '__main__':
    print('Calling the main function without TESTING set.')
    main()

    print('Calling the main function with TESTING set.')
    os.environ['TESTING'] = ''
    main()
$ python3 check_main.py 
Calling the main function without TESTING set.
The decorator was called.
The main function was called.
Calling the main function with TESTING set.
The main function was called.

As you can see, the decorator logic was executed under normal circumstances (main call on line 39) and was skipped when the TESTING environment variable was set (main call on line 43). The reason was because of the guard statement on line 21 that checks for the TESTING environment variable.

Guard Decorator

You might now say: “great, thank you David. Now I have to rewrite all of my decorator functions!” Ah, but you don’t. If you stay with me through a little more complex decorator code, you won’t have to! The idea is to write a decorator that modifies another decorator’s behaviour.

# guard_main.py
"""Demonstrates a guard decorator."""

import os


def testing_guard(decorator_func):
    """
    Decorator that only applies another decorator if the TESTING environment
    variable is not set.

    Args:
        decorator_func: The decorator function.

    Returns:
        Function that calls a function after applying the decorator if TESTING
        environment variable is not set and calls the plain function if it is set.
    """
    def replacement(original_func):
        """Function that is called instead of original function."""
        def apply_guard(*args, **kwargs):
            """Decides whether to use decorator on function call."""
            if os.getenv('TESTING') is not None:
                return original_func(*args, **kwargs)
            return decorator_func(original_func)(*args, **kwargs)

        return apply_guard
    return replacement


@testing_guard
def decorator(func):
    """
    A simple decorator that adds printing a message on a function call.

    Args:
        func: The function to decorate.

    Returns:
        The decorated function.
    """
    def replacement(*args, **kwargs):
        """Function that is called instead of original function."""
        print('The decorator was called.')
        return func(*args, **kwargs)

    return replacement


@decorator
def main():
    print('The main function was called.')


if __name__ == '__main__':
    print('Calling the main function without TESTING set.')
    main()

    print('Calling the main function with TESTING set.')
    os.environ['TESTING'] = ''
    main()
$ python3 guard_main.py 
Calling the main function without TESTING set.
The decorator was called.
The main function was called.
Calling the main function with TESTING set.
The main function was called.

As you can see, the behaviour of the code is exactly the same but the decorator is in its original form. The reason this works is because the guard decorator gets to intercept each function call and can then decide whether to first apply the decorator or call the plain function on lines 23-25.

On top of not having to re-write decorator functions which you don’t want to execute during testing, you also get to separate the logic that determines whether the decorator is applied from the decorator logic which will reduce the chances of accidentally executing decorator logic as you make changes to the decorator. It also helps you write clear unit tests for both the decorator and the guard decorator.

Working with Pytest

The last consideration is how do you apply this in practice. I would argue that, unless you are testing the decorator or guard decorator, you should always have the TESTING environment variable set. This ensures that you are only testing function logic and not decorator logic. You can achieve this by putting a fixture in your root conftest.py file with autouse set to True.

@pytest.fixture(scope='function', autouse=True)
def set_testing(monkeypatch):
    """Sets the TESTING environment variable."""
    monkeypatch.setenv('TESTING', '')

When you are testing the decorator you would always ensure that the TESTING environment variable is not set. You can achieve that using a fixture that clears the TESTING environment variable that also has autouse set to True as a part of the file that tests the decorator.

@pytest.fixture(scope='function', autouse=True)
def delete_testing(monkeypatch):
    """Deletes the TESTING environment variable."""
    monkeypatch.delenv('TESTING', raising=False)

Finally, for testing the guard decorator, make whether the TESTING environment variable is set part of the tests themselves. Don’t be shy about overriding the functionality of any autouse fixtures as a part of the test function to demonstrate what the intended state of the test is clearly.

def test_guard_decorator_testing_set(monkeypatch):
    """
    GIVEN TESTING environment variable set and ...
    WHEN ...
    THEN ...
    """
    # Setting TESTING environment variable
    monkeypatch.setenv('TESTING', '')

    # Other test code


def test_guard_decorator_testing_not_set(monkeypatch):
    """
    GIVEN TESTING environment variable is not set and ...
    WHEN ...
    THEN ...
    """
    # Setting TESTING environment variable
    monkeypatch.delenv('TESTING', raising=False)

    # Other test code

Thats it! Consider whether environment variables is the best way of indicating that decorator logic should be skipped during testing and also what the best name of the environment variable is for you. I hope this was useful to you!