Skip to content
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

MB-12189 set up fe fake data generator and factory tools #9142

Conversation

felipe-lee
Copy link
Contributor

@felipe-lee felipe-lee commented Sep 1, 2022

Jira ticket for this change

Summary

This PR sets up our FE to be able to have factories to more easily generate test data.

Document with some findings and thoughts: 🔒 https://dp3.atlassian.net/wiki/spaces/MT/pages/1895071898/Test+factories+and+fake+data+generators

Main changes
Other changes

Testing the changes

💻

Running Tests

yarn test src/pages/MyMove/Profile/ResidentialAddress.test.jsx

Verification Steps for Author

These are to be checked by the author.

  • Request review from a member of a different team.
  • Have the Jira acceptance criteria been met for this change?

We import it in a few places and with the eslint config I'm going to commit next, we need to have it in our dependencies
Set up stricter checks for extraneous deps so that we won't just allow any extraneous deps, but still allow dev dependencies where needed.
Also allow param-reassignment so that factories can more easily work with objects in the postBuild step.
@felipe-lee felipe-lee self-assigned this Sep 1, 2022
@robot-mymove
Copy link

robot-mymove commented Sep 1, 2022

Messages
📖 🔗 MB-12189

Generated by 🚫 dangerJS against 2330244

The `zipCodeByState` function uses ranges of numbers for each state to generate the zip, but since some state zip codes start with 0's, they get stripped out. This adds the zero back to the front if needed.
@@ -69,7 +83,6 @@ module.exports = {
{
files: ['*.stories.js', '*.stories.jsx', 'setupTests.js'],
rules: {
'import/no-extraneous-dependencies': 'off',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pulled this out of here and put it above so that we only allow dev dependencies but still check for other extraneous dependencies.

Comment on lines +6 to +8
const fake = (callback) => {
return perBuild(() => callback(faker));
};
Copy link
Contributor Author

@felipe-lee felipe-lee Sep 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is partially syntactic sugar. It makes it so than in a factory, instead of having to write:

...
fields: {
    streetAddress1: perBuild(() => faker.address.streetAddress())
}
...

you can instead write:

...
fields: {
    streetAddress1: fake((f) => f.address.streetAddress()),
}
...

which, tbh, imo isn't a huge win for this alone.

But, one other advantage is that it lets us set the locale once and then everything that uses fake will automatically get it. In version 3.0.0 of faker, there will be per localization packages, which will make this a little less necessary since we could just make sure we always import from the same package everywhere. Though it might still be beneficial to have it here so that we don't have to be ensuring we import from the same place and just leave that here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems worth it

export const BLANK_ADDRESS = 'blank';
export const ADDRESS_WITHOUT_COUNTRY = 'omitCountry';

const addressFactory = build({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Will engineers still be able to add specific values for certain fields? For instance with pricing wanting to choose zipcodes that are a certain distance from each other

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we should be able to do that. If you want to just pass some override values you can do that by doing something like:

const fakeAddress = addressFactory({ overrides: { streetAddress1: '1600 Washington Ave' } });

For something like the postalCode that is set as part of the postBuild, I don't think there is a way to pass in a value and set the value. Though we can just create a value using the factory and then do a manual override by doing something like:

const fakeAddress = addressFactory({ overrides: { streetAddress1: '1600 Washington Ave' } });
fakeAddress.postalCode = '90210';

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we think it's worth turning the override keys into constants? I'm working on the ServiceWorker factory, and the keys down in the weight_allotment can be tricky to get correct in an override. Constantizing them would at least make them available to Intellisense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scott and I met earlier and I learned from him that in order for an override to work, it needs to be defined as one of the base fields. So if I add postalCode to the field definitions, we can pass in an override. The postBuild can still overwrite it, but I can add a conditional to leave it as is if it's already been set. I can push up a commit with those changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also talked about having constants for the fields. I agree it would be good to do that because intellisense is nice and the point of these factories is to speed up test writing. We talked about a few options for how that could work. I'm going to push up one version, but plan on talking more about it during our PR review time on Tuesday, and bringing things up at the BE/FE checkin.

Comment on lines +6 to +8
const fake = (callback) => {
return perBuild(() => callback(faker));
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems worth it

@pearl-truss
Copy link
Contributor

Request: can a link to this PR be added to the ADR for this concept?

},
});

export default addressFactory;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale behind having this be a default import over a named import?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that was more because I started with only one export and our eslint config requires it to be default. Then I added more things, but by then I wasn't sure if it was being used in other branches so I didn't want to cause merge issues. I'm happy to change it though and just communicate out on the channel in case anyone has a branch and is using this.

Copy link
Contributor

@Ryan-Koch Ryan-Koch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Giving a 👍 for the eslintrc change

@felipe-lee
Copy link
Contributor Author

Request: can a link to this PR be added to the ADR for this concept?

Yeah, great idea! I can open up a PR for the ADR.

@felipe-lee
Copy link
Contributor Author

Request: can a link to this PR be added to the ADR for this concept?

Yeah, great idea! I can open up a PR for the ADR.

@pearl-truss here's the other PR: transcom/mymove-docs#205

return perBuild(() => callback(faker));
};

const loadSpec = (fileName) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got the idea for this from swagger-api/swagger-js#1410

Came up while I was looking for a way to use the swagger client with local files. I didn't want to point to a URL since the swagger files could change with a PR and then we'd need to update the path for those. An alternative is linked to in that thread to spin up a local server just to serve the file, but that seems like more work than is needed. Especially because once the yaml is parsed and converted to json, we don't really need the swagger client, we can access all of our definitions through the json this outputs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I tried the codegen tool a bit running:

docker run --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate \
    -i local/swagger/internal.yaml \
    -l javascript \
    -o /local/src/utils/test/swagger-client/internal \
    -DmodelDocs=false -DmodelTests=false -DapiDocs=false -DapiTests=false

It then needs to be installed with:

yarn add --dev file:./src/utils/test/swagger-client/internal

This seems to give us access to the definitions. One nice thing about this is that it gives us code completion since there are actual variables for properties like the state enum, rather than just working with json. I haven’t gotten a chance to mess with it too much though.

[ADDRESS_FIELDS.COUNTRY]: 'US', // Likely change once we support more than just CONUS moves.
},
traits: {
[ADDRESS_TRAITS.BLANK]: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't really a factory question, is there a reason we would have an address with blank fields? I see that streetAddress2 gets saved as a blank string in the customer app. Or is there some null/undefined behavior where this is useful?

It looks like we have to define all of the fields on the model and then make a trait if we ever want to remove fields down to say what is only required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that came up because when I initially started using this in the residential address tests, there had been an empty address. Though after looking at the internal yaml, it looked like the address as a whole was nullable, and could be omitted when empty, so I ended up not using this. I should probably just remove the trait since there isn't a current use case for it.

To your point about having to define all the fields and then removing them, that is what needs to be done because if you don't define a field, you can't override it at build-time, which is a bit annoying. Thanks for reminding me, I'll add that to the things that would make it easier to work with factories

};

export const addressFactory = build({
fields: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should id be included as an option here to represent an already created record?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah I need to add that one and eTag. I can push that up today.


return address;
},
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be a good pattern to start where there is a 'required' trait that doesn't automatically fill in all of the optional fields. This may not be perfect across forms but usually we are worried about validations in our tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, that would probably be a good thing to have, I can add that.

Copy link
Contributor

@duncan-truss duncan-truss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a good start to me. I am curious how it will work with nested objects but this was a good first demonstration case.

Update `NO_COUNTRY` trait to be `ONLY_BASIC_ADDRESS` because the new fields also need to be removed for cases when country was removed, at least in the tests it's being used in currently.
const loadSpec = (fileName) => {
const yamlPath = path.join(process.cwd(), `./swagger/${fileName}`);

// RA Summary: eslint - security/detect-non-literal-fs-filename - OWASP Path Traversal
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@NamibiaTorres NamibiaTorres left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job so far! I was just learning about factories in React so it was really interesting seeing how they are implemented!

@felipe-lee
Copy link
Contributor Author

Closing in favor of #9233

@felipe-lee felipe-lee closed this Sep 26, 2022
@felipe-lee felipe-lee deleted the felipe-lee/MB-12189-set-up-fe-fake-data-generator-and-factory-tools branch October 11, 2022 19:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

8 participants