-
Notifications
You must be signed in to change notification settings - Fork 9
[COL-56] Add integration tests #229
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
Conversation
dbaaeac
to
e19196e
Compare
e19196e
to
0a42b8b
Compare
0a42b8b
to
0456e4e
Compare
0456e4e
to
a20bd55
Compare
a20bd55
to
987fc91
Compare
987fc91
to
023db43
Compare
023db43
to
f126758
Compare
f126758
to
dd85414
Compare
dd85414
to
2f9332a
Compare
2f9332a
to
fba94db
Compare
fba94db
to
1dd4ab5
Compare
e005146
to
997f4c0
Compare
I’ll reuse it in the upcoming integration tests.
1dd4ab5
to
54cc3dd
Compare
test/integration/integration.test.ts
Outdated
() => { | ||
it('space members', async (context) => { | ||
// Given Spaces clients `performerSpaces` and `observerSpaces`, both configured to use the same API key, and each configured to use a different randomly-generated client ID... | ||
const [{ spaces: performerSpaces, clientId: performerClientId }, { spaces: observerSpaces }] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming is good in this file, but I think some of the comments really get in the way of following efficiently what's happening in the file. I'd review every comment and focus on two things:
- is the intent of a given line of code clear or could be part of the summary?
- is an API problematic to test, we have to work around its limitations and explain why?
There are instances of both for sure in this file, but many comments, like the one above, feel very unnecessary or could be part of the utility function, instead of being repeated multiple times per test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've slimmed down the comments and will push the updated version shortly.
test/integration/integration.test.ts
Outdated
const performerSpace = await performerSpaces.get(spaceName); | ||
const observerSpace = await observerSpaces.get(spaceName, { offlineTimeout: 5000 }); | ||
|
||
// (scenario_1_1: entering a space) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really think we should try to take advantage of nested before
and describe
blocks instead of using comments for scenarios. It makes it easier to update and understand a test section-by-section instead of having to parse the whole test to debug it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid I'm not sure exactly what it is that you're suggesting here. What it sounds like is that, given a single test that’s written linearly like this:
scenario_1:
doSomething_1
checkSomething_1
scenario_2:
doSomething_2
checkSomething_2
scenario_3:
doSomething_3
checkSomething_3
then you're suggesting that we instead write it as nested test cases something like this?
describe('scenario_1', () => {
beforeEach(() => {
// do the actions of doSomething_1 and somehow expose a way for the `it` block to check its results
});
it('some description of scenario_1', () => {
// do checkSomething_1
});
describe('scenario_2', () => {
beforeEach(() => {
// do the actions of doSomething_2 and somehow expose a way for the below `it` block to check its results
});
it('some description of scenario_2', () => {
// do checkSomething_2
});
describe('scenario_3', () => {
beforeEach(() => {
// do the actions of doSomething_3 and somehow expose a way for the below `it` block to check its results
});
it('some description of scenario_3', () => {
// do checkSomething_3
});
});
});
});
Have I got that right? If so, a couple of things that I'd wonder about:
- isn't this code going to get very indented, very quickly?
- I'm not sure how it achieves "instead of having to parse the whole test to debug it" — given that each scenario relies on the side effects given in the preceding scenarios, don't you still have to understand all of the preceding sections in order to confidently edit a given section?
If I've misunderstood what you're suggesting, would you mind perhaps showing a concrete example of what you had in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this code going to get very indented, very quickly?
this is how the unit tests are structured as well, I'm not sure why that's an issue? (I might be biased - for me this is pretty standard in JavaScript and Ruby ecosystems).
I'm not sure how it achieves "instead of having to parse the whole test to debug it" — given that each scenario relies on the side effects given in the preceding scenarios, don't you still have to understand all of the preceding sections in order to confidently edit a given section?
with your current structure, if a test fails it's something like:
cursors test failed - failed
// expectation which failed
So we need to go over all the lines of code up to the point that a given expectation has failed. Compare that to:
cursors test
assertion description and setup 1 - passed
assertion description and setup 2 - passed
assertion description and setup 3 - failed
Without looking at code or expectations, we can see what assertion, assumption fails, and also which ones are still good. With a linear setup, it's likely no assertion passes from the first failure, but that's fine. This is how Cucumber specs work in Ruby. https://github.com/ably/website/blob/main/features/authentication_a.feature#L11-L24
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just adding a comment to record that we had a chat about this and it turns out I was a bit thrown by your usage of the term "nested", where you actually just meant to keep the structure of the tests as-is but to nest each test case in a describe
, put the setup part into a beforeAll
and each scenario into its own it
. I've made those changes and will push them shortly.
test/integration/integration.test.ts
Outdated
// Note: I think that there are situations in which this listener might not get called, for the same reasons as described in commit 6e77941 — that is, the call to `performerSpace.enter()` might result in `observerSpace` seeing a presence UPDATE instead of an ENTER and hence not emitting a members `enter` event. I haven’t observed this happening in practice yet, and I’d like to be able to test that the SDK emits a members `enter` event, so I’m going to keep the test as it is for now. If we can think of a way for this test to be sure that a members `enter` event will be emitted, then we should implement it. | ||
membersEnter: new Promise<MembersEventMap['enter']>((resolve) => { | ||
observerSpace.members.once('enter', resolve); | ||
}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use the listener for all events (i.e. not pass an event to members.subscribe
) or use getAll
to check the membership? These APIs will be much more commonly used than once
in real-world apps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before I change this, I want to be sure I understand what you're suggesting. Are you saying that you don't want us to use once
at all in this test file? And, if so, would using subscribe
and then removing the subscriber when the listener is called — which would emulate the behaviour of once
and keep the test structure as it is — be OK, or are you wanting us to only call subscribe
once at the start of the test and then look at which events the listener receives as we perform various actions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and are you talking only about the members test, or all of them?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, this was just referring to members, and in particular the comment above and your worry about present
events. once
forces you to pass an event name, while you can use subscribe
to capture everything, including present
events (which honestly, as I write it, feels like a bug because it's not obvious at all).
And I proposed getAll
because it waits for the presence set to sync, I thought would be useful in tests too (as at least in theory, you can use it to expect some state to sync - same as once
). But I don't mind using once
for tests - it's still a valid approach.
To be clear, I'm not requesting any changes here, I was responding more to the comment and reference to enter
/present
behaviour.
test/integration/integration.test.ts
Outdated
// ...and that the following is true for each of the space `update` event’s member and the members `enter` event: | ||
for (const member of [eventsTriggeredByEntering.spaceUpdate.members[0], eventsTriggeredByEntering.membersEnter]) { | ||
// ...its `clientId` matches that of `performerSpaces`... | ||
context.expect(member.clientId).toEqual(performerClientId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why use context.expect
here and not expect
if we importing descirbe
and it
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was following the guidance for running tests in parallel:
When running concurrent tests, Snapshots and Assertions must use
expect
from the local Test Context to ensure the right test is detected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gotcha 👍🏻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, now that we have nested describe
blocks the tests are no longer working in parallel (will add a comment to commit message explaining what I tried), so I've switched everything back to using top-level expect
anyway.
test/integration/integration.test.ts
Outdated
context.expect(member.clientId).toEqual(performerClientId); | ||
// ...and its `profileData` matches that passed to `performerSpace.updateProfileData()`... | ||
context.expect(member.profileData).toEqual({ name: 'Luna Gomes' }); | ||
// ...and its `isConnected` continues to be true. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As an example of comments that I don't think are necessary (because the code reads almost as the English equivalent), I think all the comments from lines 86 up to here are not needed.
test/integration/integration.test.ts
Outdated
// 2. one of its `get*()` methods is called | ||
// 3. its `subscribe` or `unsubscribe` method is called | ||
// | ||
// This seems to mean that a client that sends cursor updates but does not listen for them will drop the first update passed to `cursors.set()`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is expected behaviour and your workaround is fine; if this isn't clear from docs we should update them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's clear from the docs. Is there any action needed on this PR related to this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I'll remove the TODO part of the comment)
test/integration/integration.test.ts
Outdated
const observedCursorEventsData: CursorsEventMap['update'][] = []; | ||
// TODO Decide what to do about the below comment. Should we expose the completion of the attach operation via the cursors.subscribe() API, the same way as ably-js exposes it through `presence.subscribe()`? | ||
// | ||
// Note that the `cursors.subscribe()` API does not currently provide any way for the user to know that the channel attach operation triggered by calling `presence.subscribe()` has completed. So, we have no guarantee that the `observerSpace.cursors.subscribe()` listener will receive the cursor positions sent by the calls to `performerSpace.cursors.set()` below. (However, the 1 second wait below makes it likely.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the .channel
property on cursors not be used for that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also potentially expose a method for users to have more control of channel attachment, that's synced with what subscribe/unsubscribe does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was originally hesitant about directly interacting with the underlying Ably channel in the tests, but I've changed my mind and will do so for this and for this one too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also potentially expose a method for users to have more control of channel attachment, that's synced with what subscribe/unsubscribe does.
Yep, I've updated my comment to mention this as an option.
test/integration/integration.test.ts
Outdated
// (End of setup for assertions_2_2) | ||
|
||
// ...and then wait 1 second (The idea being that this is hopefully long enough for `performerSpace.cursors` to find out about the presence enter operation triggered by calling `observerSpace.cursors.subscribe()`. I also considered instead adding a presence subscriber to `performerSpaces`’ Realtime client but wasn’t sure whether we should be messing with this client in what’s meant to be a test that somewhat simulates real-life user behaviour. If we find out that 1s is not enough, however, then I’d suggest perhaps implementing this alternative solution instead of increasing the timeout)... | ||
await setTimeout(1000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens if this timeout is removed? does the test become flaky?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, performerSpace
will drop the cursor updates and cursorUpdatesPromise
will never resolve. I've added the following text to the comment to clarify this:
and hence not drop the cursor updates that we pass to
set()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
understood 👍🏻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As alluded to here I'm going to change this to make it more deterministic by observing performerSpace.cursors.channel.presence
and removing the timeout.
test/integration/integration.test.ts
Outdated
}, | ||
}; | ||
|
||
// TODO Why is space1 emitting anything here? The emitted events make it appear as though space1 has lost the lock but space2 also failed to acquire it, which seems surprising. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a side-effect of how messages are processed right now; it is not desired but expected right now (there are tickets in the backlog to make that cleaner).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an issue that I could add a link to here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for this particular case, no - if you could open one, that would be great.
For now, I'd leave a comment here that this behaviour is expected, but not desired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created https://ably.atlassian.net/browse/COL-549 and will add a comment about it.
54cc3dd
to
3e6ef85
Compare
@dpiatek I've also added tests for |
3e6ef85
to
48ba6a9
Compare
48ba6a9
to
2dc61a1
Compare
let observerSpaces; | ||
let performerClientId; | ||
let performerSpace; | ||
let observerSpace; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One final remark @lawrence-forooghian - do you think it's worth outlining the approach taken in this test at the top of the file, similar to what you have done in the commit? I think it might not be obvious that each it
is treated as a step in the spec instead of an individual test, and they share state in a describe
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, have done.
This adds integration tests that test the core functionality of the SDK. I had originally written these as a single `describe` containing four `it`s, each of which included the test setup and then a list of scenarios, these scenarios just being separated by comments. However, Dom was keen for me to make more use of the vitest APIs for structuring my tests. This resulted in the current setup, where there is one `describe` for each area of functionality, each of these `describe`s then containing multiple `it`s. Each `it` within a given `describe` relies on the side effects of the previous `it`s within that `describe`. Dom preferred this new approach and was happy to accept the compromises that this resulted in, such as having `describe`-level local variables which have no type information, and the fact that the failure of one `it` does not prevent the subsequent `it`s (which rely on the preceding `it`s all having succeeded) from running. With my initial approach, I was able to get the tests running in parallel using `describe.concurrent` on the top-level `describe`. I wasn’t able to figure out how to achieve this in the new approach, in which we want to run all `describe`s in parallel, but run the `it`s within a given `describe` sequentially. I tried using `.concurrent` on the top-level `describe` and `.sequential` on each nested `describe`, but it still just ran everything sequentially. Resolves COL-56.
2dc61a1
to
aafe68b
Compare
This adds integration tests that test the core functionality of the SDK. It's currently a draft so I can get feedback on the approach and some of the questions that came up whilst I was working in this (see
TODO
comments in code).On top of what's currently here, I intend to also add a test for component locking.(I've added a component locking test, too.)