pact-foundation / pact-js

JS version of Pact. Pact is a contract testing framework for HTTP APIs and non-HTTP asynchronous messaging systems.
https://pact.io
Other
1.63k stars 346 forks source link

Proposal: Add NestJS Full Working Example #543

Closed omermorad closed 3 years ago

omermorad commented 3 years ago

Hi,

I have recently released a new package that connects Pact's great technology to one of the most powerful frameworks for developing server-side applications in NodeJS and TypeScript - NestJS.

The package (with 98% coverage) offers the use of two types of modules, one for the consumer and one for the provider. The modules are actually wrappers of the pact library and use the original interfaces of PactJS.

I'd love for you to take a look at the project on Github or on NPM's official website.

These days I am working on a pull-request in which I added a new folder called nestjs to the examples folder. It contains a fully end-to-end example (similar to the e2e original example) of using each of the modules (consumer and provider) in combination with Jest test runner which is the runner that NestJS guys have chosen to in the framework.

Hope to hear from you guys(!) about what you think of the code quality and practices.

Thanks in advance

TimothyJones commented 3 years ago

Thanks for this! I’m not really across NestJS or the patterns it uses (I don’t know much about angular), but adaptors to aid adoption are always useful! It’s awesome to see it’s ready for use!

Can you help me understand the value your package adds over using the DSL directly?

You might be interested to take a look at jest pact: https://github.com/pact-foundation/jest-pact - it removes the need for much of the boiler plate. From the looks of the setup in the packages doing, I don’t think you could use it directly, but it might be neat to expose a pactWith with a similar interface for consistency. What do you think?

omermorad commented 3 years ago

Thanks for this! I’m not really across NestJS or the patterns it uses (I don’t know much about angular), but adaptors to aid adoption are always useful! It’s awesome to see it’s ready for use!

Haha thanks a lot! 😎 I must say that after some investigation of Consumer Driven Contracts and Contract Testing in general, Pact is actually something we consider to use in the company!

Can you help me understand the value your package adds over using the DSL directly?

Yes of course. See, Nest is using a DI system almost identical to the one Angular uses, they use Injectable services, or to be more precise, Injectable providers.

The two main modules in this package (Consumer and Provider) wrap Pact, Verifier and Publisher, so you can inject and use some other services/providers while using them. Let's say for example that I want to create the Verifier with some custom options - if I will use the DSL directly, I would not have the ability to inject a Repository service or even a ConfigService when creating it:

@Injectable()
export class PactProducerConfigOptionsService
  implements PactProducerOptionsFactory {
  public constructor(
    private readonly animalRepository: AppRepository,
    private readonly config: ConfigService,
  ) {}

  public createPactProducerOptions(): PactProducerOptions {
    let token;

    return {
      provider: 'Animal Profile Service',
      logLevel: this.config.get('pact.logLevel'),

      requestFilter: (req, res, next) => {
        console.log(
          'Middleware invoked before provider API - injecting Authorization token',
        );
        req.headers['MY_SPECIAL_HEADER'] = 'my special value';

        // e.g. ADD Bearer token
        req.headers['authorization'] = `Bearer ${token}`;
        next();
      },

      stateHandlers: {
        'Has no animals': async () => {
          this.animalRepository.clear();
          token = '1234';
          return 'Animals removed to the db';
        },
        'Has some animals': async () => {
          token = '1234';
          this.animalRepository.importData();
          return 'Animals added to the db';
        },
        'Has an animal with ID 1': async () => {
          token = '1234';
          this.animalRepository.importData();

          return 'Animals added to the db';
        },
        'is not authenticated': async () => {
          token = '';
          return 'Invalid bearer token generated';
        },
      },

      // Fetch pacts from broker
      pactBrokerUrl: 'https://test.pact.dius.com.au/',
      consumerVersionTags: ['prod'],
      providerVersionTags: ['prod'],
      enablePending: true,
      pactBrokerUsername: '****',
      pactBrokerPassword: '*****',
      publishVerificationResult: true,
      providerVersion: '1.0.0',
    };
  }
}

In the above example you can see the power of injection when creating the Verifier configurations - I'm using the repository and the config service easily. It's actually quite difficult to explain it to someone who is not really across NestJS or the patterns it uses, but it gives a a lot of value to using Pact with NestJS.

I would be happy to add a PR that demonstrates the full usage of the package and the real value it gives.

You might be interested to take a look at jest pact: https://github.com/pact-foundation/jest-pact - it removes the need for much of the boiler plate. From the looks of the setup in the packages doing, I don’t think you could use it directly, but it might be neat to expose a pactWith with a similar interface for consistency. What do you think?

Actually I've explored the e2e example and the Jest example as well! Unfortunately you are right, I can not use it directly.

pactWith({ consumer: "MyConsumer", provider: "MyProvider" }, (provider) => { ... });

The provider argument comes from the callback function here, but if you look the the following example you will understand even better why it is not possible (you need to create the provider yourself)

describe('Pact', () => {
  let pactFactory: PactFactory;
  let provider: Pact;

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [AppModule, PactModule],
    }).compile();

    pactFactory = moduleRef.get(PactFactory);

    provider = pactFactory.createContractBetween({
      consumer: 'Consumer Service',
      provider: 'Provider Service',
    });

    const completeOptions = await provider.setup();
  });

  afterEach(() => provider.verify());

  describe('when a call to send an email from Cats Service is made', () => {
    beforeAll(() =>
      provider.addInteraction({ ... })
    );

    it('does something', () => {
      return expect(...);
    });
  });

  afterAll(() => provider.finalize());
});

Let me know if you have more questions, I will be happy to answer them and give even more explanation/example if needed!

TimothyJones commented 3 years ago

Merged in #594 :+1: