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

Allow clients to use the already used port on a standalone server #21

Conversation

ueberhammDesign
Copy link
Contributor

For test setups where you are not able to change the request url of the application that uses the mock server.
With this changes you are able to instance a client for a standalone server when the port is already used by another client.

@pimterry
Copy link
Member

Hmm, I'm not sure about this one. You're doing this so that you can get two Mockttp client instances for the same port, right?

The problem is that Mockttp client instances have state. They hold a websocket open to subscribe to handlers like thenCallback and thenStream for example, and another to listen for request events for .on('event', ...). In general being able to assume that only one client exists per server is a nice simplifying assumption that has probably been baked into all sorts of places.

I think there's probably an alternate solution. Is it not possible to call getLocal once in your test code, and use resulting server in two places, instead of calling it twice?

Is it possible for you to share the test code you're working on, so I can take a closer look?

Alternatively of course, you could use Mockttp as a proxy, which might help. Have you investigated that? In that can you can intercept the whole application, it can send requests anywhere it likes, and you can intercept them for all sorts of different URLs however you'd like.

If there's a concrete case where none of those are possible I'm open to looking at this kind of change, but I'd like to avoid it if we can.

@ueberhammDesign
Copy link
Contributor Author

ueberhammDesign commented Nov 29, 2018

I will try some of your ideas. I could not share some real code because I am not allowed to do this. What I am using is webdriver.io (selenium based test framework) as testing framework against a mocked implementation of an . I try to set up a mock server in the test set up. The problem with this framework in particular is that I could not share the instance of mockttp via the global object. The place where I am able with this framework, it tries to instance more than one mockttp server at once.

To avoid code duplications I extract the mocking methods in a single file that need a running server.

The current setup look something like this (with my changes):

Webdriver IO setup

const mockserver = mockttp.getStandalone({ allowMultiClientsOnPort: true });

[...]

// Called before startup the test workers
// not able to set global object
async onPrepare() {
    mockserver.start();
}

[...]

// Able to set the global object but it already called by all test works when tests are run in parallel.
beforeSession() {
    global.expect = require('expect');
}

[...]

afterAll() {
    mockserver.stop();
}

'mocker'

const mockserver = mockttp.getRemote({ allowMultiClientsOnPort: true });

export async function addGet(path, content, sessionId) {
    const instance = await mockserver.start(1234);
    const route = await instance.get(path).withCookie({ session: sessionId }).thenJSON(content);

   return route;
}

example test

import uuid from 'uuid';
import { addGet } from 'mocker';

describe('example test', () => {
    let sessionId;

    beforeEach(() => {
        sessionId = uuid();
        addGet('/path', { foo: 'bar' }, sessionId);
    });

    it('should do something', () => {
       browser.navigate('/consumer-for-path');

      expect(browser.find('content')).toEqual({ foo: 'bar' });
   });
});

This is the basic setup. I hope that helps by understanding the reasons for this pull request.

@chrisandrews7
Copy link

Also experiencing a similar issue using Jest.
As Jest sandboxes every suite run, it's not possible to share a single instance across multiple test suites.

@pimterry
Copy link
Member

@chrisandrews7 in general, you shouldn't really need to share an instance at all: the preferred approach is to not share a server between the tests, and start & configure a separate one within each test. Effectively to stick with Jest's philosophy, and isolate the tests from one another completely.

Mockttp supports that out of the box, is there a reason that doesn't work in your case?

If so, have you tried any of the suggestions discussed above? If you do need to share one mock server between multiple tests, and you need to reconfigure the server during those tests, then you're going to need a way to coordinate between them somehow. There's options there but they're definitely awkward, and I'd recommend avoiding it and keeping your tests independent instead, if you can.

@chrisandrews7
Copy link

@pimterry What would you suggest in the case where the application under test is being run in a docker container, and changing the url of the service I'm trying to mock is not really possible? As the test runner isnt starting/controlling the application?

@pimterry
Copy link
Member

So the application you're testing is run once for the whole test suite, and shared by all the tests? Yeah, that makes this tricky.

Given that setup I assume all your tests are running sequentially, not in parallel? As long as that's true you can do something like:

  • Start a standalone server once, before the tests run. You can do this easily by wrapping your test command with Mockttp, e.g. putting mockttp -c jest ... in your package.json script.
  • Configure the system under test to use a specific server or proxy port (8000, say)
  • In each test, start a server on that specific port
  • During the test, use that server instance and reconfigure it as required
  • At the end of each test, stop the server so the next test can use the same port

That should work for most cases, and keeps things fairly simple. Is there anything that would stop that working for you?

@chrisandrews7
Copy link

Thanks @pimterry thats what I'm currently doing. But ideally would like to run in parallel?

@pimterry
Copy link
Member

Ok, great, good start!

Doing that in parallel is hard: you've got multiple tests, unpredictably interleaving requests to the mocked server, and interleaving reconfigurations of the mocked server. Even if Mockttp & Jest both supported what you need to do this directly, it'll be messy and it'll make your tests very brittle if you're not extremely careful.

Do you definitely need to configure the server within the tests? Could you simplify and give each endpoint a preconfigured fixed response? Sharing a server is easy, if you don't need to reconfigure it during the tests.

Alternatively, is there a way you can know which requests are for which test? Maybe you know that each test only talks to a specific service or endpoint? That would make this possible: you can set up a single central mock server on a single fixed port, let each test run its own mock server on its own fixed port which it can freely reconfigure, and then preconfigure rules on the central server to forward requests to the relevant per-test mock servers (because you know by looking at the request which test is relevant). It's possible, but still difficult to do nicely.

If you don't have a way to clearly distinguish all requests, and you do need to reconfigure at runtime, this is hard and potentially impossible. For example if you have two tests which both result in requests to the same mocked endpoint, and you want to have the two tests get different responses, there's never going to be a way you can reliably run those tests in parallel - you'll never know which configuration each test will end up using.

@chrisandrews7
Copy link

chrisandrews7 commented Jun 14, 2019

Thanks @pimterry for the thorough reply. You're quite right, I think this is one of those square peg round hole situations!

@pimterry pimterry closed this Aug 19, 2019
@amoinier
Copy link

Hello @pimterry, sorry to bring up this issue.

I'm trying to do exactly what you described: have one proxy to all my parallel tests. In your last message, I'm interested in the third paragraph where you said "you can set up a single central mock server on a single fixed port, let each test run its own mock server on its own fixed port which it can freely reconfigure, and then preconfigure rules on the central server to forward requests to the relevant per-test mock servers".

I can easily know which request comes from what test (with the ID from the parameter/query/body endpoint). But I don't understand how I can forward requests from the central mock server to the "client" mock server. I would love to know how to do this.

@pimterry
Copy link
Member

Hi @amoinier!

I can easily know which request comes from what test (with the ID from the parameter/query/body endpoint). But I don't understand how I can forward requests from the central mock server to the "client" mock server.

In that case, I think you can do this with something roughly like:

centralServer.forAnyRequest().thenPassThrough({
    beforeRequest: (req) => {
        const targetUrl = calculateTargetUrl(req);
        return {
            url: targetUrl
        };
    }
});

This examines each received request, and forwards it to a different upstream server according to the details of the request (e.g. the test id you mentioned, or any other logic you want to put into calculateTargetUrl).

With that, you can send every test request from all tests to this single central mock server, and then the proxy can redirect them to the appropriate independent mock test servers for each test. Those per-test mock servers can be defined separately with any rules you like, safe in the knowledge that each is isolated from the other tests.

Does that make sense?

@amoinier
Copy link

Ok, that makes sense! I can approach my goal with that, thanks. But it's not completely perfect (it's my fault; I haven't been quite precise).

In my case, when I say "I can easily know which request comes from what test (with the ID from the parameter/query/body endpoint)", I want to say that my requests are all unique, so I will never have conflicts between my parallel tests. So with your proposal, I can't forward to a specific independent mock test server. But, It won't cause problem for me if I can forward to multiple mock test servers. Do you think it's possible to do that?

@pimterry
Copy link
Member

Hmm, I'm afraid I don't understand - I'm not sure what "forward to multiple mock test servers" means, and if every test's requests are different, I don't understand why can't you use the above code to do what you're looking for. Can you explain the full context and some more details about the issues you're facing?

@amoinier
Copy link

amoinier commented May 25, 2023

TL;DR I'm trying to set rules of requests from client mock servers, but I'm receiving requests on my global server.

I have a Node.js server app using Axios with a proxy config on port 5555 to make requests to xxx.com/user/:id. I start my app and then my E2E tests (Mocha) start making requests. In my Mocha global config, I start a proxy server to mock the xxx.com/user/:id call, and I want to mock it directly in my tests. However, with Mocha, I can't start a global mockttp server (port 5555) and access it from my parallel tests, so I need to create a client mock test server for each parallel process of tests (2 processes in my case). So I have a global mock server and two client mock servers (port 8000 & 8001).

Now, I want to mock requests from the client mock server because I can access it from my tests. But the real requests are being forwarded to my global server, and not to the client mock server (because my Node.js app is already started and the proxy config is already set to 5555). So I tried centralServer.forAnyRequest().thenPassThrough (on port 8000) to forward requests from the global server to the client server to mock it. It worked, but only for one of my process tests because the other (8001) didn't receive forwarded requests. So I tried centralServer.forAnyRequest().thenPassThrough twice to send each request to each client mock server (8000 & 8001), but it doesn't seem to work.

(I know it's a bit tricky and I shouldn't use mockttp like that, but development can be hard sometimes) ^^

@pimterry
Copy link
Member

Ah, I see, you're saying that you're trying to forward a single request to the central server to both test mock servers at the same time? That's definitely not going to be possible with the standard APIs I'm afraid. If it were, it's not clear exactly how it would work - if they both return completely different but valid responses, which one should be used?

In practice, when you define two rules like this that match the same request, by default each rule will run once (for the first received request) until the last matching rule, which will keep running forever. That's the default though, you can also tweak this using methods like .twice() or .always() while building the rule.

In this case though, I don't think you need to do any of that. From what you describe, I think this is totally possible with a single central rule, especially if it's possible to tell which server should be used from the user id in the URL. If so, you can do this with central server logic like this:

centralServer.forAnyRequest().thenPassThrough({
    beforeRequest: (req) => {
        const parsedUrl = new URL(req.url);
        const userIdMatch = /\/user\/(.*)/.exec(parsedUrl.pathname);
        if (!userIdMatch) return; // Ignore non-matching requests
        
        // Redirect to the server responsible for the relevant user id:
        const userId = userIdMatch[1];
        if (userId === "123") return { url: 'http://localhost:8000' };
        else if (userId === "456") return { url: 'http://localhost:8001' };
        else throw new Error("Unrecognized user id");
    }
});

As long as you can recognize the ids used for each test, that will do what you want. If they're not, this is still possible, but you'll need to expose this from the test servers to share it with the central server, e.g something like this:

// At the start of the test:
testMockServer1.forGet('/mock-data/userIds').thenJson(["123", "abc"]);
testMockServer2.forGet('/mock-data/userIds').thenJson(["456", "def"]);

// In the central server:
centralMockServer.forAnyRequest().thenPassThrough({
    beforeRequest: async (req) => {
        const parsedUrl = new URL(req.url);
        const userIdMatch = /\/user\/(.*)/.exec(parsedUrl.pathname);
        if (!userIdMatch) return; // Ignore non-matching requests
        
        // Query the mock servers to work out which ids to look for:
        // (you could probably cache this if it becomes problematic)
        const [server1Ids, server2Ids] = await Promise.all([
            fetch('http://localhost:8000/mock-data/userIds').then(r => r.json()),
            fetch('http://localhost:8001/mock-data/userIds').then(r => r.json())
        ]);

        const userId = userIdMatch[1];
        if (server1Ids.includes(userId)) return { url: 'http://localhost:8000' };
        if (server2Ids.includes(userId)) return { url: 'http://localhost:8001' };
        else throw new Error("Unrecognized user id");
    }
});

There's lots of other ways to do this, there are many flexible mechanisms you could use to communicate between the tests and central proxy, but I think that's probably the simplest clearest one that would do that job. Does that make sense?

@amoinier
Copy link

Wow, it works (almost) like a charm! Thanks a lot for this proposal. I'm trying to implement it correctly with my codebase; it's not perfect but the job is done. (Sorry for my late response; my scope has changed for the last two weeks and I wanted to test your solution before responding ^^)

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.

4 participants