Test Fixture Descriptors Follow protobuf-es
Applies to
@connectum/test-fixturesand@connectum/testing1.3.0.
Does this apply to you?
You need to act only if both are true:
- your tests build descriptors with
createMockDescMessage,createMockDescField,createMockDescMethodorcreateFakeMethod, or usecreateMockNext,createMockNextSlow,createMockStreamorcreateMockFn; - a test depends on the old behavior listed below, or passes a value that the helpers now refuse.
Tests that use only createMockRequest, createFakeService or assertConnectError with valid input are not affected.
What changed
| Before 1.3.0 | From 1.3.0 |
|---|---|
create(mock) returned only { $typeName } | create(mock) returns every declared field with its zero value, and each oneof group as { case: undefined } |
The field name was used verbatim as localName and jsonName | name is the proto name: user_id gives localName and jsonName userId; a localName such as toString becomes toString$ |
| Automatic field numbers were the 1-based position; a repeated number was accepted | Automatic numbers continue from the highest number so far ([{ name: "a", fieldNumber: 5 }, { name: "b" }] numbers b as 6); a repeated number throws Error |
createMockDescMethod("ToString").localName was toString | It is toString$, as for a compiled descriptor; createFakeMethod does the same |
createMockNextSlow returned the configured message object itself | It returns a copy on every call, like createMockNext |
createMockNext and createMockNextSlow copied message one level deep: nested objects were shared between responses | Each response gets a copy at every depth (plain objects, arrays, Map, Set, Date, RegExp, Buffer, typed arrays; any other nested object is still handed out as it is), so editing a nested object of one response no longer changes the next |
createMockNextError(undefined) produced an Unknown error and a number that is no Code was kept; assertConnectError skipped the message check for a messagePattern that was neither a string nor a RegExp | Both throw a TypeError naming the argument |
A type that is not a scalar name ("strng", "message", "enum") became a string field | createMockDescField and createMockDescMessage throw a TypeError that names the field, the value and the valid names |
A fieldNumber of 0, a negative number, NaN, a fraction, 19000 to 19999 or above 536870911 was accepted | It throws a RangeError that names the field and the number |
An unknown method kind (createMockDescMethod kind, createFakeMethod methodKind) was accepted | It throws a TypeError that names the method, the value and the four valid kinds |
createFakeMethod(service, "A", { register: true }) twice left two entries in service.methods | The second call throws and leaves the service unchanged |
A negative, NaN, infinite or too large delay (createMockNextSlow(delay), createMockStream delayMs) was turned into 1 ms by the timer; delayMs: -5 meant no delay | It throws (TypeError for a non-number, RangeError otherwise); 0 and an omitted delayMs still mean no delay |
The spy createMockFn called the implementation without this, recorded only arguments, and mock.calls was the internal list | It forwards this, records result, error and this of each call, and mock.calls is a fresh copy, like node:test mock.fn() |
assertConnectError reported Expected ConnectError but got ConnectError for an error of another installed copy of @connectrpc/connect | The message says that two copies of the package are probably installed |
The parent of a field built by createMockDescMessage was undefined, so toBinary, isFieldSet, equals and clone threw TypeError | Each field points back at its message through parent; those four functions accept the mock |
JSON.stringify of a mock message worked | It throws TypeError when the message has fields: the parent link is circular, as in a compiled descriptor |
proto.options of a field with isSensitive: true was { debug_redact: true } | It is a google.protobuf.FieldOptions message with debugRedact: true |
file.proto.edition was the string "EDITION_PROTO3" | file.edition is Edition.EDITION_PROTO3 (999); file.proto.syntax is "proto3" and file.proto.edition is Edition.EDITION_UNKNOWN (0) |
Names that are already camelCase (userId, email) are unchanged.
Required changes
A strict comparison of
create(mock)with a bare{ $typeName }now fails. List the zero values, or compare the fields you set:typescriptconst mock = createMockDescMessage('acme.v1.User', { fields: [{ name: 'id', type: 'int32' }, { name: 'email' }], }); // Before: assert.deepStrictEqual(create(mock), { $typeName: 'acme.v1.User' }) assert.deepStrictEqual(create(mock), { $typeName: 'acme.v1.User', id: 0, email: '' });Look up a snake_case field by its camelCase name:
desc.field.userId, notdesc.field.user_id. The proto name stays available asfield.name.Give every field a distinct
fieldNumber, or leave the numbers out. Two fields with one number throw when the message is created.Look up a method with a reserved name under its escaped
localName(toString$).Serialise
typeNameand the field names instead of the descriptor object when a test passes a mock message toJSON.stringify. Readproto.options.debugRedact(notdebug_redact) of a sensitive field, and the edition fromfile.edition.useSensitiveRedactionofcreateMockDescMethodis deprecated and keeps its behaviour; mark a field withisSensitiveinstead.If a test mutated a
messagereturned bycreateMockNextorcreateMockNextSlow, at any depth, and expected the configured object, or the next response, to change, change the configured object (or pass a newmessage) instead; every response is now a copy at every depth.Fix the value a refused call names. The factories now throw where they used to build something else:
typescriptcreateMockDescField('f', { type: 'strng' }); // TypeError: unknown scalar type "strng"; expected one of: double, float, ... createMockDescMessage('acme.v1.M', { fields: [{ name: 'a', fieldNumber: 19000 }] }); // RangeError: field number 19000 is not allowed createFakeMethod(service, 'X', { methodKind: 'bogus' }); // TypeError: unknown method kind "bogus" createMockNextSlow(-5); // RangeError: delay must be a finite number of milliseconds between 0 and 2147483647In TypeScript,
typeis now theMockScalarTypeunion of the fifteen scalar names; astringvariable needs a narrowing or a cast. For a field of a message type or an enum there is no mock: a scalar stands in for it.Use
0(or leavedelayMsout) where a test passed a negativedelayMsto mean no delay.A call record of
createMockFnnow has four keys (arguments,result,error,this), as innode:test. Replaceassert.deepStrictEqual(fn.mock.calls, [{ arguments: [1] }])withassert.deepStrictEqual(fn.mock.calls.map((call) => call.arguments), [[1]]), and readfn.mock.callsagain after each call: it is a copy, so a list read earlier does not grow. A hand-writtenMockCallliteral needs the new fields.
Limits that remain
- A mock oneof group has no fields (
fields: []), and the declared fields are not attached to it. - A custom
json_nameoption cannot be expressed;jsonNameis always derived from the name. createMockRequeststill buildshttp://localhost/<service>/<method>; a request from the in-process transport carries anhttps://in-memory/…origin. Passurlexplicitly if the code under test depends on the origin.
Verify
Run your test suite against @connectum/test-fixtures 1.3.0. For a stronger check, compile the same message with a real .proto and compare create(realDescriptor) with create(mock).
See also @connectum/test-fixtures.