Model-Based Testing with Playwright
Model-Based Testing structures tests around two key abstractions: a model that holds the data used in the test, and a page object that represents the UI of the system under test. The combination of a predictable data structure and encapsulated actions enables clean, reusable tests, even as a UI evolves over time.
export interface PortfolioPageModel {
title?: RegExp | string;
heroText?: RegExp | string;
projects?: Array<{
title: string;
company: string;
}>;
skillsVisible?: boolean;
}import { type Page, expect } from '@playwright/test';
import type { PortfolioPageModel } from '../models/portfolioPageModel';
export class PortfolioPage {
private readonly heading =
this.page.getByRole('heading', { level: 1 });
private readonly projectTiles =
this.page.locator('.project-tile');
private readonly skillsSection =
this.page.getByRole('heading', { name: 'Skills' });
constructor(private readonly page: Page) {}
async goto() {
await this.page.goto('/');
}
async validateAgainstModel(model: PortfolioPageModel) {
if (model.title)
await expect(this.page).toHaveTitle(model.title);
if (model.heroText)
await expect(this.heading).toContainText(model.heroText);
if (model.projects) {
for (const project of model.projects) {
await expect(
this.projectTiles.filter({ hasText: project.title })
).toBeVisible();
}
}
if (model.skillsVisible)
await expect(this.skillsSection).toBeVisible();
}
}import { test } from '@playwright/test';
import { PortfolioPage } from './pages/portfolioPage';
test('portfolio homepage renders correctly', async ({ page }) => {
const portfolio = new PortfolioPage(page);
await portfolio.goto();
await portfolio.validateAgainstModel({
title: /Matthew Grant/,
heroText: /quality at every layer/i,
projects: [
{ title: 'Data & Reporting Platform', company: 'MRI Software' },
{ title: 'Data Migration Project', company: 'Proptech Group Limited' },
{ title: 'CRM & Contracts Platform', company: 'Harcourts International' },
],
skillsVisible: true,
});
});public class PortfolioPageModel
{
public string? Title { get; set; }
public string? HeroText { get; set; }
public List<ProjectItem>? Projects { get; set; }
public bool? SkillsVisible { get; set; }
}
public record ProjectItem(string Title, string Company);using Microsoft.Playwright;
using static Microsoft.Playwright.Assertions;
public class PortfolioPage(IPage page) : PageBase(page)
{
private ILocator Heading => Page.GetByRole(AriaRole.Heading, new() { Level = 1 });
private ILocator Projects => Page.Locator(".project-tile");
private ILocator Skills => Page.GetByRole(AriaRole.Heading, new() { Name = "Skills" });
public Task Goto() => Page.GotoAsync("/");
public override async Task ValidatePageAgainstModel(PortfolioPageModel model)
{
if (model.Title is not null)
await Expect(Page).ToHaveTitleAsync(new Regex(model.Title));
if (model.HeroText is not null)
await Expect(Heading).ToContainTextAsync(model.HeroText,
new() { IgnoreCase = true });
if (model.Projects is not null)
foreach (var project in model.Projects)
await Expect(
Projects.Filter(new() { HasText = project.Title })
).ToBeVisibleAsync();
if (model.SkillsVisible is true)
await Expect(Skills).ToBeVisibleAsync();
}
}using NUnit.Framework;
[TestFixture]
public class PortfolioTests : TestBase
{
private PortfolioPage _portfolio;
[SetUp]
public async Task SetUp()
{
_portfolio = Pages.Portfolio;
await Task.CompletedTask;
}
[Test]
[Description("Portfolio homepage renders correctly.")]
public async Task TestHomepageRendersCorrectly()
{
var model = new PortfolioPageModel
{
Title = "Matthew Grant",
HeroText = "quality at every layer",
Projects =
[
new() { Title = "Data & Reporting Platform", Company = "MRI Software" },
new() { Title = "Data Migration Project", Company = "Proptech Group Limited" },
new() { Title = "CRM & Contracts Platform", Company = "Harcourts International" },
],
SkillsVisible = true,
};
await _portfolio.Goto();
await _portfolio.ValidatePageAgainstModel(model);
}
} $ Waiting to run…