Skip to content

Improve factories#2922

Open
thomasiles wants to merge 8 commits into
mainfrom
improve-factories
Open

Improve factories#2922
thomasiles wants to merge 8 commits into
mainfrom
improve-factories

Conversation

@thomasiles

Copy link
Copy Markdown
Contributor

Improvements to our test Factories

Trello card: https://trello.com/c/L37Bdby6/260-improve-the-tests-in-forms-admin

Our FactoryBot tests have become a bit tangled. I'm trying to uncouple them a bit and stop using the database when it's not needed and stop hardcoding database ids, which, I think, causes reliability problems when tests are run in certain orders or at the same time.

This PR tackles some of the issues which stop us using build, which doesn't use the database instead of create. It doesn't switch any tests yet, it's just preparing the groundwork.

One of the biggest issues is how we treat organisations. I've tried to keep the commits small but one commit makes small changes to a lot of specs which relied on orgs being the same.

You can use the rails console to test the factories with Factorybot.create or FactoryBot.build enabling SQL logging is also useful:

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::LogSubscriber.attach_to :active_record

Things to consider when reviewing

  • Ensure that you consider the wider context.
  • Does it work when run on your machine?
  • Is it clear what the code is doing?
  • Do the commit messages explain why the changes were made?
  • Are there all the unit tests needed?
  • Do the end to end tests need updating before these changes will pass?
  • Has all relevant documentation been updated?

Remove the hardcoded organisation id, which I *think* but haven't
confirmed is the reason for the test being flaky.
The Form factory trait, :with_pages builds a new form for each page
which isn't used.

To see the issue, open the rails console compare the number of forms
before and after using the factory and trait.

```ruby
Form.count
f=FactoryBot.create(:form, :with_pages)
Form.count
```

You will see six forms created when only one should be made.

Instead we build the pages and set the form to nil.

Enable logging SQL statements in the console to see how much extra work
is involved without this change.

```ruby
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::LogSubscriber.attach_to :active_record
```
Remove hardcoded id which can cause deadlocks in the database when two
tests run concurrently.

We also updates the membership factory to build associations and share one
organisation instance between user, group, and added_by.

Without this, build(:membership) would compare two in-memory
Organisation objects with nil ids (now that the hardcoded id=1 is gone)
and fail the 'User and group must have the same organisation'
validation.

Sharing a user means build(:membership) touches no tables and
create(:membership) no longer races two Organisation inserts against the
unique slug index.
The organisation factory dedups every default-built organisation onto a
single canonical test-org row, so specs could create users (via auth
helpers) and groups (via the bare :group factory) and get the same
organisation by accident.

We want to remove the dedup part of the Organisation factory, so that
we can build them without touching the database.

This commit changes specs to make the sharing explicit.
Our specs used to rely on the fact that orgs with the same slug would be
identical.

Now that we've removed that pattern from the specs and made it explicit,
we can remove code from the factory that was ensuring that all orgs
created with the same slug were identical.

This caused the DB to be used, even when using build as the select would
always be called.

Instead, we give organisations a unique sequence-based slug.
Using create in a factory means even factories built with 'build' or
'build_stubbed' will hit the database.
User's belong to Organisations except when they first sign up.

We have validation that ensures that once a user has an organisation
they move to a new organisation but can't be without one.

The existing validation accessed the database column directly. This
meant if you build a User and add an Organisation to it, it will be
invalid.

Instead, we add a check to only run the validation if there is no
Organisation yet.
The MOU signature factory was using create to make a user, which
meant using build would still use the database. This caused us to use
create in Org, too.

We needed to use create to make the user valid because of the validation
on User. Now we've fixed that, we can use build instead.

We also change the org factory.

Because we are now using build instead of create for users, the view
specs failed when using a path helper that relied on the user's id.

We fix that by passing build_stubbed(:user) in the specs so there is an
id.
@github-actions

Copy link
Copy Markdown

🎉 A review copy of this PR has been deployed! You can reach it at: https://pr-2922.admin.review.forms.service.gov.uk/

It may take 5 minutes or so for the application to be fully deployed and working. If it still isn't ready
after 5 minutes, there may be something wrong with the ECS task. You will need to go to the integration AWS account
to debug, or otherwise ask an infrastructure person.

For the sign in details and more information, see the review apps wiki page.

@thomasiles thomasiles marked this pull request as ready for review July 14, 2026 12:55
Comment thread spec/models/user_spec.rb
context "and the user's organisation is not the same as given" do
it "returns false" do
user = create(:organisation_admin_user)
other_org = build(:organisation, id: 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We've got the hardcoded org id, which could clash now?

let(:user_with_access_admin) { create(:user, email: "admin_user@example.com", organisation: signed_mou_org) }
let(:user_without_access) { create(:user, email: "inactive_user@example.com", has_access: false, organisation: signed_mou_org) }
let(:user_access_and_admin_with_mou) { create(:user, email: "admin_mou_user@example.com", organisation: signed_mou_org) }
let(:non_crown_org) { create(:organisation, id: 2, slug: "non-crown-org") }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Drop the hard coded org id here? as will now collide with signed_mou_org?

Comment thread app/models/user.rb

def requires_organisation?
organisation_id_was.present? || role_changed?(to: :organisation_admin)
organisation.blank? && (organisation_id_was.present? || role_changed?(to: :organisation_admin))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is going to add extra db query every time, this function is called - is that okay?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants