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

[instrumentation-fetch] refactor fetch() tests for clarity, type safety and realism #5268

Merged
merged 1 commit into from
Jan 7, 2025

Conversation

chancancode
Copy link
Contributor

@chancancode chancancode commented Dec 14, 2024

Which problem is this PR solving?

Refactor fetch() tests for improved clearity, type safety and realism (relative to the real-world fetch behavior).

I would like to propose something for #4888 to start a discussion, but in doing so I find that the current tests are quite confusing to work with, so I split out these refactors to the tests as a separate PR/proposal.

I think this represents a significant enough improvements to the tests that it makes sense as a PR, but I may something else that I want to try/propose next week in the longer term, I am also proposing that we further refactor these tests in #5282.

Short description of the changes

See the inline commits on the PR.

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • Internal refactor

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

  • npm run lint
  • npm run test:browser

Checklist:

  • Followed the style guidelines of this project
  • Unit tests have been added updated
  • Documentation has been updated

@chancancode chancancode requested a review from a team as a code owner December 14, 2024 02:39
chancancode added a commit to tildeio/opentelemetry-js that referenced this pull request Dec 14, 2024
Refactor `fetch()` tests for improved clearity, type safety and
realism (relative to the real-world `fetch` behavior).

See the inline comments on PR open-telemetry#5268 for a detailed explaination of
the changes.
@@ -176,7 +176,7 @@ function testForCorrectEvents(

describe('fetch', () => {
let contextManager: ZoneContextManager;
let lastResponse: any | undefined;
let lastResponse: Response | undefined;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This changes more than the type! Despite the name, this variable previously wasn't storing the Response object of the last fetch() request. It stores something that is kind of the parsed response body (~= await response.json()) but also with some additional bespoke processing/normalization that didn't turn out to be necessary.

This changes things so lastResponse actually stores the Response object from the last fetch() request made in the test. Any test that cares about the response body can call await lastResponse.json() in the test itself and do what it needs to do there.

@@ -210,12 +210,21 @@ describe('fetch', () => {
sinon.stub(core.otperformance, 'now').callsFake(() => fakeNow);

function fakeFetch(input: RequestInfo | Request, init: RequestInit = {}) {
// Once upon a time, there was a bug (#2411), causing a `Request` object
// to be incorrectly passed to `fetch()` as the second argument
assert.ok(!(init instanceof Request));
Copy link
Contributor Author

@chancancode chancancode Dec 14, 2024

Choose a reason for hiding this comment

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

This is a replacement for the init instanceof Request branch below. We don't have this bug anymore, but this assertion will catch it if it regress, and instantly fail all the tests (synchronously).

const response: any = {
args: {},
url: fileUrl,
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This variable is really the "default response body", but it also serves a double-duty as the ResponseInit (second argument to new Response(), which is very confusing. I separated those into appropriately named separate variables.

To be clear, no tests actually cares that the mock "server response body" has the status in it.


// This is the default response body expected by the few tests that cares
let responseBody = JSON.stringify({
isServerResponse: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

args: {} in the diff is only used by a single test, and all it does with it is assert.deepEquals(parsedResponseBody.args, {}). Maybe it used to do more, but I inferred that these days the only purpose it serves is "did I get the mock response I expected", and so I replaced that with this field.

// Passing request as 2nd argument causes missing body bug (#2411)
response.status = 400;
response.statusText = 'Bad Request (Request object as 2nd argument)';
reject(new window.Response(JSON.stringify(response), response));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This branch was replaced entirely. Note that:

  1. It is testing for the bug in fix(instrumentation-fetch): fetch(string, Request) silently drops request body #2411 which has now be fixed, so this branch is never exercised in practice.
  2. To avoid a regression, I replaced the spirit of this with the assert.ok at the top.
  3. This behavior is misleading and in correct – the real fetch() never reject() the fetch promise with a response. Even when the server returns an HTTP error (response.ok === false), the promise is still resolved.

In any case, this branch is not needed anymore.

}

resolve(new window.Response(responseBody, responseInit));
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 now always resolve, as the real fetch() would. The 404 branch is also never exercised in the tests (as evident by the fact that I changed it from resolve() to reject() and nothing broke). As mentioned above, rejecting with a response is never correct.

If we are rejecting here because it's an internal assertion that indicating some test is set up wrong, I'd rather see this reject with an regular Error, or just throw synchronously at the top.

Object.keys(lastResponse.headers).forEach(key => {
headers[key.toLowerCase()] = lastResponse.headers[key];
});
lastResponse.headers = headers;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the convoluted normalization that didn't turn out to be needed (other than removing this part, most of the diff is just indentation change from removing the now-unecessary try/catch.

As far as I can tell, (other than the previously unfortunate variable naming) all this would do is to normalize the request headers into lower case. However, nothing broke when I stopped doing this, so evidently the two tests that cares don't actually need this.

assert.strictEqual(
lastResponse.headers[X_B3_TRACE_ID],
request.headers[X_B3_TRACE_ID],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previously badly named variable made this very confusing, but this is actually asserting that we made the request with the correct headers. Specifically that when the URL is not ignored, the instrumentation code will add these additional headers (which then gets put into the mock server response).

assert.fail(response.statusText);
}
);
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test was added for #2411. It is basically testing that nothing breaks when passing a Request object into fetch works. However, that pattern is now ubiquitously used in numerous other tests (e.g. see around L550), so I'm not sure this is still necessary anymore. Also as mentioned about, this test is not very realistically written, as the real fetch() would never reject() with a Response like this.

Copy link

codecov bot commented Dec 14, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 94.62%. Comparing base (90afa28) to head (5ee5d8f).
Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5268   +/-   ##
=======================================
  Coverage   94.62%   94.62%           
=======================================
  Files         323      323           
  Lines        8068     8068           
  Branches     1637     1637           
=======================================
  Hits         7634     7634           
  Misses        434      434           

chancancode added a commit to tildeio/opentelemetry-js that referenced this pull request Dec 18, 2024
Refactor `fetch()` tests for improved clearity, type safety and
realism (relative to the real-world `fetch` behavior).

See the inline comments on PR open-telemetry#5268 for a detailed explaination of
the changes.
@chancancode chancancode changed the base branch from next to main December 18, 2024 19:48
@chancancode
Copy link
Contributor Author

rebased and retargeted

Copy link
Member

@pichlermarc pichlermarc left a comment

Choose a reason for hiding this comment

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

This looks great, thank you for cleaning up the tests here. 🙌

I have to admit that I lack a lot of context on the @opentelemetry/instrumenation-fetch and @opentelemetry/instrumenation-xhr packages as they pre-date my time on the project, so the comments you provided were a immensely helpful in figuring out what's going on. Reviewing and having to gather context would've taken me at lot longer if they had not been there.

@pichlermarc
Copy link
Member

Ah, looks like there's some conflict somewhere (I can't resolve due to maintainer edits being off).

cc @open-telemetry/javascript-approvers if you see the conflicts resolved while I'm ooo please give it another quick look merge this in, thanks 🙂

CHANGELOG.md Outdated Show resolved Hide resolved
chancancode added a commit to tildeio/opentelemetry-js that referenced this pull request Dec 20, 2024
Refactor `fetch()` tests for improved clearity, type safety and
realism (relative to the real-world `fetch` behavior).

See the inline comments on PR open-telemetry#5268 for a detailed explaination of
the changes.
@chancancode
Copy link
Contributor Author

Removed the CHANGELOG entry (need a Skip Changelog label now) thus the conflict

Refactor `fetch()` tests for improved clearity, type safety and
realism (relative to the real-world `fetch` behavior).

See the inline comments on PR open-telemetry#5268 for a detailed explaination of
the changes.
@pichlermarc pichlermarc added this pull request to the merge queue Jan 7, 2025
Merged via the queue into open-telemetry:main with commit 9c46e8f Jan 7, 2025
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants