Migrating Schema Tests To @ngneat/falso: A Comprehensive Guide

by SLV Team 63 views

Hey everyone! Today, we're diving deep into migrating schema tests to @ngneat/falso factories. This guide will walk you through the entire process, covering everything from the project context and objectives to the migration pattern, checklist, and crucial rules. Whether you're aiming for 100% migration or just exploring the benefits, this article has got you covered. So, let's get started!

🎯 Objective: Streamlining Schema Tests

Our main goal here is to migrate 20 schema test files from hardcoded data to centralized factories using the amazing @ngneat/falso library. This will significantly improve our Zod validation tests by using more realistic and dynamically generated data. Think of it as leveling up our testing game!

Why This Matters

By using @ngneat/falso, we can avoid manually creating test data, which can be time-consuming and prone to errors. Factories allow us to generate consistent, realistic data quickly, making our tests more robust and maintainable. This is a crucial step in ensuring the reliability and accuracy of our data validation.

πŸ“‹ Project Context: Camino Service Backend

Let's quickly set the stage. We're working on the Camino Service Backend, a project built with Next.js, Clean Architecture, TypeScript, and Supabase. It's a modern stack designed for scalability and maintainability. Here’s a snapshot of our migration progress:

  • βœ… Services: 18/18 migrated (100% complete)
  • βœ… Repositories: 21/21 migrated (100% complete - Phase 2)
  • βœ… Controllers: 17/17 migrated (100% complete - Phase 3)
  • ⏳ Schemas: 0/20 migrated (0% - THIS PHASE - OPTIONAL)

Overall, we're sitting at a solid 74% completion rate (56/76 files migrated). But there's a twist – this schema migration phase is marked as optional.

Understanding the Optional Phase

Now, you might be wondering why this phase is optional. There are several reasons for this:

  1. Lower Benefit: Schemas primarily validate structure and have less hardcoded data compared to other layers.
  2. Lower Priority: The migration of Services, Repositories, and Controllers already covers a significant 74% of the project.
  3. Lower Complexity: Schema tests are generally simpler, with less code duplication.
  4. Team Decision: Ultimately, it’s up to the team to decide if the effort is worth it compared to other pressing tasks.

The recommendation here is straightforward: only migrate if you have available time after completing Phases 1-3, want to achieve 100% migration, or identify significant hardcoding in schema tests. It’s all about balancing effort and impact.

πŸ“š Reference Documentation: Your Best Friend

Before we dive into the tasks, let's talk about documentation. Seriously, this is critical. Make sure to read these resources first:

  1. Complete Migration Guide: __tests__/helpers/README.md (792 lines of pure gold!)
    • Pay close attention to the section: "Fase 4: Schemas (Prioridad MEDIA - opcional)"
    • Check out the pattern: "PatrΓ³n 7: Tests de Schemas (Opcional)"
    • Don't forget the checklist: "Para Schemas (Opcional)"
  2. Available Factories: __tests__/helpers/factories.ts
    • There are 20 factories ready and waiting for you!
    • Utilize methods like .create(), .createDto(), and .createMany().
  3. Completed Examples:
    • Services: __tests__/services/*.test.ts (all migrated)
    • Repositories: __tests__/repositories/*.test.ts (all migrated)
    • Controllers: __tests__/controllers/*.test.ts (all migrated)

Seriously, read the docs! They'll save you a ton of time and headaches.

🎯 Tasks to Complete: The Migration To-Do List

Okay, if we decide to go ahead with the migration, here are the files we need to tackle (20 in total).

Files to Migrate

Here's the list in alphabetical order (no special prioritization here):

  1. βœ… __tests__/schemas/availability.schema.test.ts
  2. βœ… __tests__/schemas/booking.schema.test.ts
  3. βœ… __tests__/schemas/camino.schema.test.ts
  4. βœ… __tests__/schemas/csp.schema.test.ts
  5. βœ… __tests__/schemas/favorite.schema.test.ts
  6. βœ… __tests__/schemas/inventory.schema.test.ts
  7. βœ… __tests__/schemas/inventory_items.schema.test.ts
  8. βœ… __tests__/schemas/partner.schema.test.ts
  9. βœ… __tests__/schemas/payment.schema.test.ts
  10. βœ… __tests__/schemas/precio.schema.test.ts
  11. βœ… __tests__/schemas/producto.schema.test.ts
  12. βœ… __tests__/schemas/report.schema.test.ts
  13. βœ… __tests__/schemas/review.schema.test.ts
  14. βœ… __tests__/schemas/service_assignment.schema.test.ts
  15. βœ… __tests__/schemas/taller_manager.schema.test.ts
  16. βœ… __tests__/schemas/user.schema.test.ts
  17. βœ… __tests__/schemas/vending_machine.schema.test.ts
  18. βœ… __tests__/schemas/vending_machine_slot.schema.test.ts
  19. βœ… __tests__/schemas/venta_app.schema.test.ts
  20. βœ… __tests__/schemas/workshop.schema.test.ts

πŸ“ Migration Pattern: The Blueprint for Success

To ensure consistency and avoid chaos, we need to follow a specific migration pattern. This is mandatory, guys. Let’s look at the before and after:

BEFORE (Hardcoded Data):

describe('createUserSchema', () => {
  it('should validate valid user data', () => {
    const validData = {
      email: "test@example.com",
      full_name: "Test User",
      role: "user",
    };

    expect(() => createUserSchema.parse(validData)).not.toThrow();
  });

  it('should reject invalid email', () => {
    const invalidData = {
      email: "invalid-email",
      full_name: "Test User",
      role: "user",
    };

    expect(() => createUserSchema.parse(invalidData)).toThrow();
  });

  it('should reject missing required fields', () => {
    const invalidData = {
      email: "test@example.com",
      // full_name missing
      role: "user",
    };

    expect(() => createUserSchema.parse(invalidData)).toThrow();
  });
});

AFTER (Using Factories):

import { UserFactory } from '../helpers/factories';

describe('createUserSchema', () => {
  it('should validate valid user data', () => {
    const validData = UserFactory.createDto();
    expect(() => createUserSchema.parse(validData)).not.toThrow();
  });

  it('should reject invalid email', () => {
    const invalidData = UserFactory.createDto({ email: "invalid-email" });
    expect(() => createUserSchema.parse(invalidData)).toThrow();
  });

  it('should reject missing required fields', () => {
    const invalidData = UserFactory.createDto({ full_name: undefined as any });
    expect(() => createUserSchema.parse(invalidData)).toThrow();
  });
});

Notice the difference? We're replacing hardcoded values with factory-generated data. This is the magic right here.

βœ… Migration Checklist: Your Step-by-Step Guide

To make sure we're on the right track, let's break down the migration process into a checklist. Here’s what you need to do for each file:

For Each File:

  1. βœ… Import the factory: import { XFactory } from '../helpers/factories'
  2. βœ… Valid cases: createXSchema.parse(XFactory.createDto())
  3. βœ… Invalid cases: XFactory.createDto({ email: "invalid" })
  4. βœ… Test edge cases with specific overrides
  5. βœ… Maintain all Zod validation assertions
  6. βœ… Run the test: npm test -- __tests__/schemas/X.schema.test.ts
  7. βœ… Verify that ALL tests pass
  8. βœ… Commit individually: test(schemas): migrate X.schema.test to factories

🚨 Critical Rules: No Negotiations!

These aren't suggestions, guys. These are rules. Break them at your own peril:

1. ALL Tests MUST Pass

# After migrating EACH file, run:
npm test -- __tests__/schemas/ARCHIVO.schema.test.ts

# Expected result: PASS with X/X tests
# If any test fails, DO NOT continue until you fix it

2. Complete Migration BEFORE the PR (if you decide to migrate)

  • ❌ NO creating a PR if all 20/20 files aren't migrated
  • ❌ NO creating a PR if any test fails
  • ❌ NO creating a PR without updating the documentation

3. Mandatory Final Validation

# BEFORE creating the PR, run:
npm test -- __tests__/schemas/

# Expected result: 20/20 suites PASS, X/X tests PASS

4. Update Documentation Upon Completion

Modify __tests__/helpers/README.md:

### Schemas (20/20 migrated - COMPLETED) βœ…

- βœ… `user.schema.test.ts` - X tests
- βœ… `booking.schema.test.ts` - X tests
- βœ… `payment.schema.test.ts` - X tests
[... all the others ...]

**Progress**: 100% completed (20/20 files) | **Tests**: X/X passing βœ…

Update the summary table:

| Layer          | Total Files | Migrated | Pending | Progress | Priority    |
| -------------- | ----------- | -------- | ------- | -------- | ----------- |
| Services       | 18          | 18 βœ…   | 0       | 100% πŸŽ‰ | Completed   |
| Repositories   | 21          | 21 βœ…   | 0       | 100% πŸŽ‰ | Completed   |
| Controllers    | 17          | 17 βœ…   | 0       | 100% πŸŽ‰ | Completed   |
| Schemas        | 20          | 20 βœ…   | 0       | 100% πŸŽ‰ | Completed   | <- UPDATE
| TOTAL          | 76          | 76       | 0       | 100%     | -           | <- UPDATE

πŸ“Š Success Criteria: How We Measure Victory

Let’s define what success looks like. If we decide to migrate, we need to meet these objectives:

Objectives to Meet (if you decide to migrate):

  • βœ… 20/20 schema files migrated
  • βœ… 100% tests passing (verify with npm test -- __tests__/schemas/)
  • βœ… Zero residual hardcoded data
  • βœ… Valid cases using .createDto()
  • βœ… Invalid cases using overrides
  • βœ… Updated README.md documentation
  • βœ… Individual commits per file

Expected Metrics:

  • Migration Coverage: 100% (76/76 total files) πŸŽ‰
  • Hardcode Reduction: ~80% in schemas
  • Tests Passing: 100% (no regressions)

πŸŽ“ Additional Resources: Common Patterns

To help you along the way, let's look at some common patterns in schema tests:

Common Patterns in Schemas:

1. Validating Valid Data

it('should validate valid data', () => {
  const validData = UserFactory.createDto();
  expect(() => createUserSchema.parse(validData)).not.toThrow();
});

2. Validating Invalid Email

it('should reject invalid email', () => {
  const invalidData = UserFactory.createDto({ email: "not-an-email" });
  expect(() => createUserSchema.parse(invalidData)).toThrow();
});

3. Validating Required Fields

it('should reject missing required field', () => {
  const invalidData = UserFactory.createDto({ full_name: undefined as any });
  expect(() => createUserSchema.parse(invalidData)).toThrow();
});

4. Validating String Length

it('should reject string too short', () => {
  const invalidData = UserFactory.createDto({ full_name: "a" }); // Min 2
  expect(() => createUserSchema.parse(invalidData)).toThrow();
});

it('should reject string too long', () => {
  const tooLongName = "x".repeat(200); // Max 150
  const invalidData = UserFactory.createDto({ full_name: tooLongName });
  expect(() => createUserSchema.parse(invalidData)).toThrow();
});

5. Validating Enums

it('should reject invalid role', () => {
  const invalidData = UserFactory.createDto({ role: "invalid_role" as any });
  expect(() => createUserSchema.parse(invalidData)).toThrow();
});

6. Validating UUIDs

it('should reject invalid UUID', () => {
  const invalidData = { id: "not-a-uuid", ...UserFactory.createDto() };
  expect(() => updateUserSchema.parse(invalidData)).toThrow();
});

πŸ” Pre-PR Validation: The Final Sanity Check

Before you even think about creating a PR, run all these commands:

# 1. Schema tests
npm test -- __tests__/schemas/
# Expected: 20 passed, 20 total

# 2. General tests (ensure no regressions)
npm test
# Expected: ALL tests passing

# 3. Lint
npm run lint
# Expected: 0 errors

# 4. Verify modified files
git status
# Expected: 20 schema.test.ts files + README.md

πŸ“ Commit Format: Keep It Clean

Individual commits per file are the way to go:

test(schemas): migrate user.schema.test to factories
test(schemas): migrate booking.schema.test to factories
test(schemas): migrate payment.schema.test to factories
[... etc]

And for the final documentation commit:

docs(tests): update README with schemas migration completion (20/20) - 100% TOTAL

⚠️ Common Mistakes to Avoid: Steer Clear!

Let's quickly run through some common pitfalls so you don't fall into them:

  1. ❌ DO NOT use .create() for schemas (use .createDto())
  2. ❌ DO NOT modify existing Zod validations
  3. ❌ DO NOT delete edge case tests
  4. ❌ DO NOT create a PR without completing all files
  5. ❌ DO NOT ignore failing tests
  6. ❌ DO NOT forget to update the README.md upon completion

🎯 Expected Final Result: The Promised Land

If we complete this phase, here’s what we’ll have:

Upon Completion of This Phase:

  • 20/20 schema files migrated βœ…
  • Total progress: 100% (76/76 files) πŸŽ‰πŸŽ‰πŸŽ‰
  • Tests: 100% passing with no regressions
  • README.md updated with the completed Schemas section
  • COMPLETE PROJECT MIGRATION

This PR would achieve 100% migration of all layers of the project. Talk about a mic drop!

πŸ“Œ Important Notes: Key Takeaways

  • This phase is OPTIONAL – evaluate the priority against other tasks
  • Estimated time: ~30-45 minutes for all 20 files
  • Lower benefit compared to other phases (schemas have less hardcode)
  • Each migrated file is an individual commit for easier review
  • Review the README.md constantly for pattern-related questions

πŸ€” Evaluation: To Migrate or Not to Migrate?

Let's break this down with a simple decision matrix:

Migrate IF:

  • βœ… You want to achieve 100% migration completion
  • βœ… You have available time without compromising other priorities
  • βœ… You value total project consistency
  • βœ… You detect significant hardcoding in schemas

DO NOT Migrate IF:

  • ❌ There are higher priority tasks (features, bugs)
  • ❌ The benefit is marginal (little hardcode in schemas)
  • ❌ The 74% migration (Phases 1-3) is sufficient
  • ❌ You prefer pragmatism over perfectionism

Recommendation: Complete Phases 1-3 first, then evaluate if Phase 4 provides real value.

REMEMBER: ONLY CREATE A PR IF YOU DECIDE TO MIGRATE AND COMPLETE ALL 20/20 FILES!

Happy migrating, folks! And remember, quality code is a team sport!