Testing
This page is about testing an application that uses Breeze. For running Breeze's own test suite, see the end of this page.
Most of what your code asks of Breeze needs no server. Once metadata is loaded, the following all run entirely on the client:
- creating entities
- navigation fix-up
- change tracking
- validation
- querying the cache
- export and import
For the parts that do talk to a server, you can hand Breeze a stub fetch function.
The examples use Vitest and the Northwind model.
Configure Breeze once
With a .NET server, Breeze needs no configuration: the default adapters and the default camelCase naming convention are what it needs, in tests as in the application. If your application does configure Breeze — a custom adapter, say, or NamingConvention.none for a server that already sends client property names — do the same in a Vitest setup file. Breeze configuration is global state, so the tests then use what the application uses:
// test/breeze-setup.ts
import { configureBreeze, NamingConvention } from 'breeze-client';
// the same configuration as your application's startup, for example:
configureBreeze({ namingConvention: NamingConvention.none });// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
setupFiles: ['./test/breeze-setup.ts'],
},
});Breeze runs in Node 20+, so the default node environment is fine. You don't need a DOM.
Load metadata from a fixture
Save your model's metadata as a JSON file and check it in. Either:
- request
<serviceName>/Metadatafrom your running server once and save the response, or - call
em.metadataStore.exportMetadata()in a session that has loaded metadata. It returns a JSON string.
It must be Breeze JSON metadata, the kind with structuralTypes. Breeze 3 does not parse CSDL/EDMX.
Build one MetadataStore from the file and give each test its own empty manager that shares it:
// test/breeze-helpers.ts
import { DataService, EntityManager, MetadataStore } from 'breeze-client';
import metadata from './fixtures/metadata.json';
const serviceName = '/breeze/NorthwindIBModel';
export const metadataStore = new MetadataStore();
metadataStore.importMetadata(metadata);
metadataStore.addDataService(new DataService({ serviceName }));
export function newEntityManager() {
return new EntityManager({ serviceName, metadataStore });
}- The metadata is parsed once for the whole file.
- Each test gets a fresh cache, so no test sees another's entities.
addDataServicetells the store it already has metadata forserviceName, and a manager created with thatserviceNameuses thisDataService. Without it, Breeze would tryGET <serviceName>/Metadatabefore the first remote query, because a file saved from/Metadatanames no service. A file fromexportMetadata()carries the exporting store's data services, so importing it is enough.
If your application registers entity classes with registerEntityTypeCtor, register them on this shared store, once. A class can only be registered with one MetadataStore. See Extending entities.
Test entity logic
With metadata loaded, entity behavior is synchronous and needs no network:
import { describe, expect, test } from 'vitest';
import { EntityQuery, EntityState } from 'breeze-client';
import { newEntityManager } from './breeze-helpers';
describe('customers and orders', () => {
test('a new order is linked to its customer', () => {
const em = newEntityManager();
const cust = em.createEntity(Customer, {
customerID: crypto.randomUUID(),
companyName: 'Acme',
});
const order = em.createEntity(Order, { customerID: cust.customerID });
expect(cust.orders).toContain(order);
expect(order.entityAspect.entityState).toBe(EntityState.Added);
expect(em.getChanges()).toHaveLength(2);
});
test('editing an unchanged customer marks it Modified', () => {
const em = newEntityManager();
const cust = em.createEntity(
'Customer',
{ customerID: crypto.randomUUID(), companyName: 'Acme' },
EntityState.Unchanged,
);
cust.companyName = 'Bravo';
expect(cust.entityAspect.entityState).toBe(EntityState.Modified);
expect(cust.entityAspect.originalValues).toEqual({ companyName: 'Acme' });
});
test('a customer needs a company name', () => {
const em = newEntityManager();
const cust = em.createEntity(Customer, { customerID: crypto.randomUUID() });
expect(cust.entityAspect.validateEntity()).toBe(false);
expect(cust.entityAspect.getValidationErrors()[0].errorMessage)
.toBe("'companyName' is required");
});
test('the cache can be queried', () => {
const em = newEntityManager();
em.createEntity(Customer, { customerID: crypto.randomUUID(), companyName: 'Acme' });
const found = em.executeQueryLocally(
EntityQuery.from(Customer).where('companyName', 'startsWith', 'Ac'),
);
expect(found).toHaveLength(1);
});
});Passing EntityState.Unchanged to createEntity attaches the entity as if it had come from the server. That is useful for building "existing" data. See Querying the cache, Change tracking and Validation.
Seed test data with export and import
To start several tests from the same set of entities, build them once, export them, and import the bundle into each test's manager:
import { beforeAll, test } from 'vitest';
import { EntityState } from 'breeze-client';
import { newEntityManager } from './breeze-helpers';
let seed: string;
beforeAll(() => {
const em = newEntityManager();
const alfreds = em.createEntity(Customer, {
customerID: crypto.randomUUID(), companyName: 'Alfreds Futterkiste',
}, EntityState.Unchanged);
em.createEntity(Order, {
orderID: 10643, customerID: alfreds.customerID,
}, EntityState.Unchanged);
seed = em.exportEntities(undefined, { includeMetadata: false }) as string;
});
test('...', () => {
const em = newEntityManager();
em.importEntities(seed);
// em now holds the customer and order, both Unchanged
});Imported entities keep their entity state, so pending changes can be seeded too.
A bundle can also be a fixture file. Write it out once with exportEntities(entities, { asString: false }). If you leave includeMetadata at its default of true, the static EntityManager.importEntities(bundle) returns a new manager with the metadata and entities already loaded. See Export and import.
Stub HTTP with a fetch function
configureBreeze takes a fetch function, and Breeze uses it for every request. A test double is an ordinary function that returns a Response. The basic pattern is on Supplying your own transport. A few details matter when the stub has to return entities:
- Use server property names. Response data is what the server would send. With the default
NamingConvention.camelCasethat meansCompanyName, notcompanyName. Camel-case names in the payload are not mapped and come through asnull. - Include
$type, as the Breeze .NET server does, so Breeze knows which entity type each object is. The type name isNamespace.TypeName, Assembly. - Return an array of entities, or
{ results, inlineCount }for queries that useinlineCount(). - The query is URL-encoded JSON after the
?in the resource URL, in server property names. Decode it to assert on what was sent.
import { beforeAll, expect, test } from 'vitest';
import { configureBreeze, BreezeFetch, EntityQuery } from 'breeze-client';
import { newEntityManager } from './breeze-helpers';
const requests: string[] = [];
const fakeFetch: BreezeFetch = async (input) => {
const url = String(input);
requests.push(url);
const body = url.includes('/Customers?')
? [{
$type: 'Northwind.Models.Customer, Northwind',
CustomerID: 'b5e0a4a0-0000-0000-0000-000000000001',
CompanyName: 'Alfreds Futterkiste',
}]
: [];
return new Response(JSON.stringify(body), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
};
beforeAll(() => {
configureBreeze({ fetch: fakeFetch });
});
test('customer search', async () => {
const em = newEntityManager();
const { results } = await em.executeQuery(
EntityQuery.from(Customer).where('companyName', 'startsWith', 'A'),
);
expect(results[0].companyName).toBe('Alfreds Futterkiste');
const sent = JSON.parse(decodeURIComponent(requests[0].split('?')[1]));
expect(sent).toEqual({ where: { CompanyName: { startswith: 'A' } } });
});To test error handling, return a non-2xx Response, for example new Response('boom', { status: 500 }). executeQuery then rejects, and you can assert with await expect(em.executeQuery(q)).rejects.toThrow().
saveChanges sends a POST to <serviceName>/SaveChanges with a JSON body whose entities array holds each changed entity. Asserting on that body is simple. Returning a realistic save result means reproducing your server's response JSON, so capture a real one from your browser's network tab and use it as a fixture.
Breeze's own tests
To run the Breeze client test suite itself (unit tests with no server, plus integration tests against the .NET test server), see TESTING.md in the repository.