TripleSpeeder
TripleSpeeder17mo ago

Testing functions

Hey there, I'm curious how/if people are testing actual convex mutations and queries? For example I have some rather complex mutations for upserting docs following 2-3 levels of relations which would really benefit from some automated testing. Are there any best practices or recommendations for this?
5 Replies
Michal Srb
Michal Srb17mo ago
Hey @TripleSpeeder, we're still working on better testing support for functions. Hopefully won't be much longer before we have something for you. For now if you could share the kind of code you'd like to test, and the assertions that you'd like to make, that would help us design the testing framework. Thanks!
TripleSpeeder
TripleSpeederOP17mo ago
I think the keypoint i want to address is to make sure that object relations are modified correctly. Here is a trimmed down version of my schema, focussing on the relations:
manufacturer: defineTable({
name: v.string(),
}),

bike: defineTable({
userId: v.id("users"),
manufacturerId: v.id("manufacturer"),
}),

part: defineTable({
bikeId: v.id("bike"),
userId: v.id("users"),
eventId: v.optional(v.id("event")),
manufacturerId: v.id("manufacturer"),
replaces: v.optional(v.id("part")),
replacedBy: v.optional(v.id("part")),
}),

event: defineTable({
userId: v.id("users"),
bikeId: v.id("bike"),
}),

image: defineTable({
userId: v.id("users"),
storageId: v.string(),
targetId: v.string(),
}),

like: defineTable({
userId: v.id("users"),
targetId: likeTargetValidator,
targetTableName: likeTargetTableNameValidator,
})
manufacturer: defineTable({
name: v.string(),
}),

bike: defineTable({
userId: v.id("users"),
manufacturerId: v.id("manufacturer"),
}),

part: defineTable({
bikeId: v.id("bike"),
userId: v.id("users"),
eventId: v.optional(v.id("event")),
manufacturerId: v.id("manufacturer"),
replaces: v.optional(v.id("part")),
replacedBy: v.optional(v.id("part")),
}),

event: defineTable({
userId: v.id("users"),
bikeId: v.id("bike"),
}),

image: defineTable({
userId: v.id("users"),
storageId: v.string(),
targetId: v.string(),
}),

like: defineTable({
userId: v.id("users"),
targetId: likeTargetValidator,
targetTableName: likeTargetTableNameValidator,
})
For example I want to test this: DeleteEventMutation: If the user deletes an event, the mutation has to take care that: - all likes for the event are deleted - all images for the event are deleted - All actual files (storageId) get deleted - All parts attached to the deleted event get their "eventId" set to undefined It's more complex when user edits e.g. an event. User can add, edit and delete parts for an event, and edit the event itself all with one mutation. This is working remarkably fine using react-hook-forms with the useFormField() hook. I have created an upsertMutation for the different entities that either update an existing entry or create a new entry, depending if they are called with a valid ._id in args. Basically what I would like to have is test coverage on these CRUD cases to make sure that i correctly update/create/delete all related entities.
Michal Srb
Michal Srb17mo ago
Makes total sense, thanks for providing the details!
winsoroaks
winsoroaks16mo ago
hello @TripleSpeeder can you pls share how do you the upsertMutation? im interesting in implementing that but it involves at least a read + a write, right?
TripleSpeeder
TripleSpeederOP16mo ago
Yes, if an _id is provided I'm trying to load and patch it, if it is not provided I'm creating a new entry. Here is a pretty simple example:
type upsertManufacturerProps = {
ctx: GenericMutationCtx<DataModel>;
manufacturerData: Infer<typeof upsertManufacturerValidator>;
};
export const upsertManufacturer = async ({
ctx,
manufacturerData,
}: upsertManufacturerProps) => {
let manufacturerId: Id<"manufacturer">;
if (manufacturerData._id) {
// Update
const manufacturer = await ctx.db.get(manufacturerData._id);
if (!manufacturer)
throw new Error(`Manufacturer ${manufacturerData._id} not found`);
await ctx.db.patch(manufacturerData._id, {
name: manufacturerData.name,
// update other fields as required/provided in manufacturerData.
});
manufacturerId = manufacturerData._id;
} else {
// Create
// TODO prevent creation of duplicate manufacturer
manufacturerId = await ctx.db.insert("manufacturer", {
name: manufacturerData.name,
// set other data
});
}
return manufacturerId;
};
type upsertManufacturerProps = {
ctx: GenericMutationCtx<DataModel>;
manufacturerData: Infer<typeof upsertManufacturerValidator>;
};
export const upsertManufacturer = async ({
ctx,
manufacturerData,
}: upsertManufacturerProps) => {
let manufacturerId: Id<"manufacturer">;
if (manufacturerData._id) {
// Update
const manufacturer = await ctx.db.get(manufacturerData._id);
if (!manufacturer)
throw new Error(`Manufacturer ${manufacturerData._id} not found`);
await ctx.db.patch(manufacturerData._id, {
name: manufacturerData.name,
// update other fields as required/provided in manufacturerData.
});
manufacturerId = manufacturerData._id;
} else {
// Create
// TODO prevent creation of duplicate manufacturer
manufacturerId = await ctx.db.insert("manufacturer", {
name: manufacturerData.name,
// set other data
});
}
return manufacturerId;
};
mind the TODO when creating new objects. As Convex DB has no concept llike SQLunique constraint you need to implement a manual check for existing objects.

Did you find this page helpful?