In this post, we’ll look into Material Data Tables. In our example below, we’re going to set up a basic UI with a few rows in our table, and write some high level tests.
What do we use data tables for? On Google’s Material site it describes the data table as
Data tables display sets of data across rows and columns.
https://material.io/components/data-tables/
And from a usage perspective, Google says
Data tables display information in a grid-like format of rows and columns. They organize information in a way that’s easy to scan, so that users can look for patterns and insights.
https://material.io/components/data-tables/#usage
Data Tables give us a tool to show information in an organized way to help make reviewing that data more efficient.
The Code
So that we don’t get mired in UI details, our interface is extremely light. It contains only a data table with a material header showing our column names, and mat rows with our data. Once that UI is set up, adding other components such as buttons and drop downs is trivial, as is the testing of those elements.
<table mat-table [dataSource]="ourAnimals" id="testTable"> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef>Name</th> <td mat-cell *matCellDef="let animal">{{ animal.name }}</td> </ng-container> <ng-container matColumnDef="type"> <th mat-header-cell *matHeaderCellDef>Type</th> <td mat-cell *matCellDef="let animal">{{ animal.type }}</td> </ng-container> <ng-container matColumnDef="size"> <th mat-header-cell *matHeaderCellDef>Size</th> <td mat-cell *matCellDef="let animal">{{ animal.size }}</td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr> </table>
Our component is as light as it can get. Here we only have a property holding our allowed columns to display, as well as a property holding our data. Again, to keep things light and focused we haven’t added any functionality beyond what we need to test the UI pieces of this. In the real world, you would probably have a service with a subject or observable, and in this component you would subscribe to that subject or observable to get your data.
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-datatable-ut', templateUrl: './datatable-ut.component.html', styleUrls: ['./datatable-ut.component.css'] }) export class DatatableUtComponent implements OnInit { displayedColumns: string[] = ['name', 'type', 'size']; ourAnimals = [ { name: 'Bear', type: 'animal', size: 'large' }, { name: 'Dog', type: 'animal', size: 'medium' }, { name: 'Cat', type: 'animal', size: 'small' }, { name: 'Elephant', type: 'animal', size: 'xLarge' } ]; constructor() {} ngOnInit() {} }
The tests
One thing you might notice about the code below, is that we have a dataDump variable, as well as validAnimals variable. This is so that we can test real world data from an api or database, as well as formalize what our expected end result is, for any type of filtering or specific service calls. The setup for testing our datatable is quite simple, we ensure we have our component, DatatableUtComponent, as well as our MatTableModule, imported and declared in the TestBed configuration. We then jump right into our tests.
We might be tempted to test some of the built in functionality of the data-table. This would include things like pagination and sorting. We have to be cautious about delving into the realm of testing other people’s code, particularly when it’s part of a larger library provided to us. Thinking through this, we might add in a test to verify the initial state has pagination, or that the initial state is sorted a certain way, but we don’t do that. Here what we want to be sure of, is that we are displaying the appropriate data and interactive elements if they exist, with the data elements that we expect. In our example, we want the animal data to be displayed initially, with no interactive elements, and it should display the Name, Type and Size elements.
In our first test, we want to be sure it displays all of our animals, but only our animals. We start by finding the mat-table. In our code we added an id, which is a good standard practice to find our elements under test more easily. Here our mat-table has the id “testTable”. We then get all of our mat-row elements, and transform those into an Array. This is possible because the resulting element structure is iterable. Having these in an Array will make it easier to work with the data structure for our tests.
const ourDomTableUnderTest = document.querySelector('table#testTable'); const animalsInTable = Array.from( ourDomTableUnderTest.getElementsByClassName('mat-row') );
We then iterate through our array, animalsInTable, and test that every animal we expect is in the table. Here we find our data through the class, mat-column-{column name}, so for us that’s mat-column-name, mat-column-type and mat-column size. We save that data for each row of data and then build our expect call. What we expect is that our row will be contained in our validAnimals variable. When testing this for your data, you can choose a subset of columns in your data table that will make a unique entry, and verify it’s in the validation array using the jasmine.objectContaining helper. This ensures that an object, with a subset of all properties of that object, is contained in a larger array.
animalsInTable.forEach(animal => { const animalName = animal .getElementsByClassName('mat-column-name') .item(0).textContent; const animalType = animal .getElementsByClassName('mat-column-type') .item(0).textContent; const animalSize = animal .getElementsByClassName('mat-column-size') .item(0).textContent; expect(validAnimals).toContain( jasmine.objectContaining({ type: animalType, name: animalName, size: animalSize })
Our next test validates that all columns are present in our table. We do this in case someone added or removed columns we weren’t expecting. To start this check, it’s much like the above. Here we find the mat-header-cell instead of mat-column-{name}, and save that as an array. We then set up a validation variable, here called headerClasses, with the names of our expected columns. There might be better ways to do this, but we spell out our full expected column names.
const tableHeaders = Array.from( ourDomTableUnderTest.getElementsByClassName('mat-header-cell') ); const headerClasses = [ 'mat-column-name', 'mat-column-type', 'mat-column-size' ];
The first thing to notice is that our code which checks against what we are expecting has a flaw. There’s a gap here, we check that each expected header is in the list, but not that there are extra headers we don’t expect. I’ll leave that exercise to you, dear reader, to solve. For us, we move forward. For our test, validating that the headers present are what we expect, we iterate through our Array of headers on the page, tableHeaders. We then use the headerClasses.some[] method, available on arrays, and write a quick callback function to validate each expected header with the classList that the header contains. How are we doing this? Our element, named header, is a DomTokenList which makes a contains() method available for us. So for each expected header item in the callback, we simply check if the classList contains that item.
tableHeaders.forEach(header => { expect( headerClasses.some(item => header.classList.contains(item)) ).toBeTruthy(); });
And here’s our full spec.
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { DatatableUtComponent } from './datatable-ut.component'; import { MatTableModule } from '@angular/material/table'; describe('ui-noninteractive - DatatableUtComponent', () => { let component: DatatableUtComponent; let fixture: ComponentFixture<DatatableUtComponent>; const dataDump = [ { name: 'Bear', type: 'animal', size: 'large' }, { name: 'Hamburger', type: 'food', size: 'small' }, { name: 'Dog', type: 'animal', size: 'medium' }, { name: 'Cat', type: 'animal', size: 'small' }, { name: 'Hotdog', type: 'food', size: 'small' }, { name: 'Sheetcake', type: 'food', size: 'medium' }, { name: 'Burrito', type: 'food', size: 'grande' }, { name: 'Elephant', type: 'animal', size: 'xLarge' } ]; const validAnimals = [ { name: 'Bear', type: 'animal', size: 'large' }, { name: 'Dog', type: 'animal', size: 'medium' }, { name: 'Cat', type: 'animal', size: 'small' }, { name: 'Elephant', type: 'animal', size: 'xLarge' } ]; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [DatatableUtComponent], imports: [MatTableModule] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(DatatableUtComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should show our animal data', () => { const ourDomTableUnderTest = document.querySelector('table#testTable'); const animalsInTable = Array.from( ourDomTableUnderTest.getElementsByClassName('mat-row') ); animalsInTable.forEach(animal => { const animalName = animal .getElementsByClassName('mat-column-name') .item(0).textContent; const animalType = animal .getElementsByClassName('mat-column-type') .item(0).textContent; const animalSize = animal .getElementsByClassName('mat-column-size') .item(0).textContent; expect(validAnimals).toContain( jasmine.objectContaining({ type: animalType, name: animalName, size: animalSize }) ); }); }); it('should show the columns we expect', () => { const ourDomTableUnderTest = document.querySelector('table#testTable'); const tableHeaders = Array.from( ourDomTableUnderTest.getElementsByClassName('mat-header-cell') ); const headerClasses = [ 'mat-column-name', 'mat-column-type', 'mat-column-size' ]; tableHeaders.forEach(header => { expect( headerClasses.some(item => header.classList.contains(item)) ).toBeTruthy(); }); }); });
Wrapup
Material datatables are perfect for showing datasets with many elements. While we used the plain datatable here, you can add interactive elements, buttons, dropdowns, to the table to add more options to what you can do with the data. We also didn’t dive into sorting or pagination here. From a testing perspective, while we would typically test the functionality a bit deeper than we do here, and in a much more BDD way, we are only concerned with the basics of mat-table testing. Like many other elements, adding in testing of the content of a mat-table is made easier once you know how to test the basic functionality. The rest is mundane. Always remember though, our goal in testing shouldn’t be individual element based it should be looking for outcomes of a workflow. Testing a user expectation. So the tests outlined above should be part of a larger “should” and shouldn’t be the ends itself.
All of the code shown here, and more Angular tests, can be found in this GitHub repo.