-
-
Notifications
You must be signed in to change notification settings - Fork 919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document how to write factory functions with value overwrites #1508
Comments
Thanks for sharing a real world usage scenario with us, this really helps us to focus on and provide features that you are using or missing during your work every day. To sum up your feature proposal in pseudo code: mock_T_Factory<T> = (overwrite: T) => deepMerge( sample_T_Generator(), overwrite); IMO this generally a good idea, however I would like to probe into a few design decisions that you have made and that could deviate from other developers expectations.
|
I can share the code with you once I checkout it is okay internally to share. Just a formality I suppose. But to respond to the probe, your pseudo code is very close to accurate, there are a few tweaks:
Regarding omitting optional properties, yeah this is likely the most controversial part of the code but in our experience it helps make mock factories predictable and level. If I create a mock factory Example: interface Address {
address1: sting;
address2?: string;
}
interface Person {
name: string;
age?: number;
address?: Address;
phone?: string;
}
const mockAddress = createMockFactory<Address>({
address1: faker.address.streetAddress(),
});
const mockPerson = createMockFactory<Person>({
name: faker.name.fullName(),
}); With the current proposed implementation, every instance of mockAddress will contain exactly {
address1: string;
address2: never;
} and every instance of mockPerson will contain exactly {
name: string;
age: never;
address: never;
phone: never;
} I can make explicit assumptions about 100% of calls to the factory functions now. If I were to always randomly decide which optional props existed or didn't if I wanted to make a test case that verified if a given optional property is undefined, I would have to explicitly define that. This makes the override less useful because instead of dealing only with expect(mockPerson({age: undefined}).age).toStrictEqual(undefined); // works every time but is a lot more properties I have to define
expect(mockPerson().age).toStrictEqual(undefined); // maybe true maybe not if we randomly define it on each call This problem becomes more and more difficult the deeper nested object you get expect(mockPerson().address.address2).toStrictEqual(undefined); // I have 2 probabilities to deal with now In practice using a it('tests with an address', () => {
const myAddress2 = faker.address.streetSuffix();
expect(mockPerson({
address: {
address2: myAddress2
}
}).address.address2).toStrictEqual(myAddress2);
}) I could see a more flexible version of this approach to instead allow some function createMockFactory<T>(defaults: T, {randomness: number = 0}) {
return function(overrides: DeepOptional<T>) {
return map_values_with_randomness(deepMerge(defaults, overrides));
}
} We discussed these topics while developing the function and decided that the simpler architecture was better in that it doesn't block you from doing anything, you can always specify whatever you want in the override. But like I said, if there is significant push back from a larger community of devs that have different preferences I think we can adjust to accommodate that. What do you think would be the best next steps for something like this? Should I open a PR with a sample implementation? Or what would you suggest? |
We will discuss this in our team meeting tomorrow. |
Team decision Due to the different expectations regarding the method, we won't add it to our repository directly. |
Would you be able to elaborate on I would like to use this feedback in continuing development. I will go ahead and open source it independently, maybe there could be a future where the docs link to it? ;) |
Sure, no problem. Please let us know, if you disagree with any of these points.
Here a few examples, Usage Example const user = mockUser({
name: 'foobar',
organization: {
id: 15,
}
}); const user = createUser();
user.name = 'foobar';
user.organization.id = 15; Implementation Example export const mockUser = createMockFactory<User>(() => {
name: faker.internet.userName(),
organization: mockOrganization();
}, optionalMergeStrategyHere); // <-- Not sure how to implement this/how readable the implementation will be
const optionalMergeStrategyHere = (path, key, valueOld, valueNew) => path === '.' && key === 'organization' ? 'DEEP_MERGE' : valueNew; // Overrides Strategy: Merge
export function createUser(overrides: DeepPartial<User>) {
return {
name: faker.internet.userName(),
...overrides,
organization: createOrganization(overrides.organization);
};
} // Overrides Strategy: Replace
export function createUser(overrides: DeepPartial<User>) {
return {
name: faker.internet.userName(),
organization: overrides.organization ?? createOrganization();
};
} Options Example export const mockUser = createMockFactory<User>(() => {
name: faker.internet.userName(),
});
export const mockUserWithOrganization = createMockFactory<User>(() => {
name: faker.internet.userName(),
organization: mockOrganization();
}); export function createUser(overrides: DeepPartial<User>, options: { includeOrganization?: boolean } = {}) {
const { includeOrganization } = options;
return {
name: faker.internet.userName(),
...overrides,
organization: faker.datatype.boolean(includeOrganization) ? createOrganization(overrides.organization) : undefined;
};
} |
Are these points up for rebuttal or is this discussion closed RE including in faker? I think there some advantages missed that I can write up when I have some time. |
We are always interested in new points of view and suggestions from our contributors and users. Could you please explain what the
|
RE is like in an email, as in All the code here is psuedo code. I just tried to give as much as needed to understand the use case and API. I've gotten a green light for opensourcing the code so far so can put it in a repo and share soon. That being said, here we go :) Sorry if it is long, there is just a lot to unpack. Comparing
|
Overrides and Merging
Yes, that the overrides option for createUser is intentional, as it shows how easy that is to implement yourself, no need for a special factory method that does things that are hard to read/spread over multiple places.
This is not really about specifying vs setting, as that is just an implementation detail IMO as long as you have the override option you can do it one way or the other. Optional Properties
IMO it should be other way round. You are testing something that probably a user will specify, so you don't know the exact shape of the object. It might have the optional property or not, you don't know. The same is true for test data. Lets say you add a new optional property to your datamodel that affects something worth checking. If we allow the methods to contain optional properties, then the user can decide whether it should include that property or not. Use Faker in the Implementation
I'm not sure what you are referring to here. IMO there is no difference between calling faker.person.firstName and calling faker.helpers.maybe. Also AFAICT there is no programatic way to check if a property is optional or not, so you have to actually implement that yourself.
IMO passing a faker instance to the factory (creator) or when using the factory is actually an interesting idea, although you could just pass it to a method almost identically. One of the benefits here is actually in combination with #1499 faker.derive that ensures that only a single value is taken from the seed and thus the methods would produce the same values even if a previous method generate an additional property. export function firstName({ faker: Faker, sex?: SexType }) {
[...]
}
// ---
export function bind<Params, Result>(fn: ({faker: Faker} & Params) => Result, faker: Faker): (options: Params) => Result {
return (options) => fn({...options, faker});
} We will discuss this concept in our next team meeting again. As for generating a factories from annotations, this is an entirely different proposal. Merge Strategies
So we are basically back to the method without factory? export function createUser(overrides: DeepPartial<User>, options: { includeOrganization?: boolean } = {}) {
const { includeOrganization } = options;
return {
name: faker.internet.userName(),
...overrides,
organization: faker.helpers.maybe(() => createOrganization(overrides.organization), includeOrganization);
};
} I myself would like to see your ideas for a Here are some (somewhat soft) requirements for the two methods:
Please be aware, that the PR is currently unlikely to be merged at all, might require heavy rewrites / evolutions before it gets merged, might only be partially be merged or only some concepts get merged and might stay unmerged for a long time. |
Clear and concise description of the problem
Most who use faker end up wrapping faker methods into "factory functions" that compose those methods into usable objects that represent their underlying data needs.
Example in faker docs itself: https://fakerjs.dev/guide/usage.html#create-complex-objects
There are several concepts in this pattern that could be extracted into a relatively lightweight method that could ship along with faker giving first class support and a strong foundation for this use case being implemented "correctly" (I'll lay out my argument for why there should be a correct way to do this"
Suggested solution
We implemented a version of this in our team internally and would like to open source it. Currently there is no option to opens source within our company, so I wanted to propose we add it directly into faker. The method is implemented and tested already and the actual code is very short so it would likely be a low maintenance addition.
Below I've added our README we use to document the method. The actual method is just a few lines and is just a simple deep merge. It is currently implemented using
deepmerge
but could be refactored to be without dependencies.Most of the magic is in the custom types. These types enforce good best practices when creating complex objects by leveraging typescript to enforce only required properties to be defined by default and giving an optional override argument to the factory function to allow for defining optional properties.
<----- Here is the README for our implementation ----->
title: Writing Tests with Mock Factories
Getting started
When you want to create a new mock, create a
fixtures
folder in the component directory andplace an
index.ts
in it. Mocks can be shared between unit tests and stories.Folder structure
Then create the mock by passing required properties:
Finally, use the mock into your test file or stories:
Important: do not export the mock factories through the component. Doing so might cause the
staging and production builds to fail, as the
/tests
folder is not resolved in those builds.Conventions:
mock${InterfaceName}
so an interface with nameFooBar
would have amock factory named
mockFooBar
Props
so component with nameFoo
would have amock factory named
mockFooProps
fixtures
and export named mock factories from anindex.ts file in this folder.
components/MyComponent/fixtures
index.ts file.defaults
argumentUse
createMockFactory<T>
to generate reusable, composable, and strongly typed mock data objectsin your stories or unit tests. To create a mock factory, just pass a typescript interface and
your
defaults
data to thecreateMockFactory
method. This method will type check your data,to make sure it correctly implements all and only all required properties of the interface.
The
defaults
argument will accept either a static object, or a function that will be calledeach time a mock is created, allowing for randomized or dynamic data. The default will also restrict
your default values to required props only, so in the example below,
optional
must be set toundefined in the "default" data set. This ensures that the base set of data for an object covers all
and only all of the required properties, making the "default" case more accurately reflect how
typescript will handle your interface.
overrideValues
argumentContinuing with the example from above, call
mockProps()
to generate mock objects from thefactory.
mockProps()
is a generic function, so it also accepts a type ReturnType, the default valuefor ReturnType is, well T.
What does that mean? It means that if you call
mockProps()
with no generic argument, it assumesyou want the function to return a value of the interface the mock is mocking. You can also pass
in any interface that extends
T
.That means you could pass in
DeepOmitOptional<T>
and this would mean mockProps() would only beallowed to return required properties. you could also pass in
T & SuperT
whereSuperT
addsadditional properties.
Here is an example:
The
overrideValues
argument that is aDeepPartial
of your interface.Using a partial of your interface allows each mock instance to override specific properties
and merge them with the values from
defaults
. A deep partial allows the properties tobe overwritten at any level in the data structure.
Typescript will throw an error that the property optional incorrectly implements the type
expected, because types boolean and undefined are incompatible.
NOTE:
Your IDE will make creating mock objects really easy if you have the typescript language service enabled. This will allow for visual feedback in your editor, as well as autocomplete of your data objects.
Composing mocks
You can compose mock factories into larger mock factories. This can reduce code duplication
and make your data much more consistent and reliable. Let's say you define a mockFactory for
MoneyObject
:You could then use mockMoneyObject(); in the mockProps factory default argument.
Now each time you create a mock from mockProps, the nested property will use mockMoneyObject to
define its data. Because nested expects the type
DeepOmitOptional<MockMoneyObject>
our call tomockMoneyObject()
must return only required data.Instead of having to continuously redefine values for nested interfaces, you can define just a
single mock factory for a typescript interface, and share the mocking logic into any number of use
cases that depend on that interface. Notice though, that if you don't explicitly use
DeepOmitOptional<T>()
in these composition cases, the value being passed to a parent "defaults"argument, will throw an error on the property, instead of the value.
Compare to alternatives
You might ask, why use a fancy factory method to create some mock objects? Why not just use
static values? Let's compare two code snippets. One using a more basic, 'on-the-fly' getter
function, versus a mock factory.
The key differences are that in the second case you rely on less boilerplate code, you achieve a
much higher type safety, and you have much more flexibility in how you design your mocks. Default
values exclude optional properties, giving you very reliable mock data. Using a factory method for
generating the mocks themselves makes the solution scalable and easy to adopt.
Allowing static and dynamic objects retains the option of simple static mocks, but further increases
the reliability of the mock with dynamic values, since you don't have to rely on a single value,
but a range of values that match the type criterion.
Alternative
I could open source this under a separate repo, but it just feels so close to faker it made sense to at least open the discussion if this would be something that could belong in faker.
Additional context
No response
The text was updated successfully, but these errors were encountered: