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

Add test coverage for lib/table.js #178

Merged
merged 10 commits into from
Jun 10, 2020
Merged

Conversation

rmeritz
Copy link
Contributor

@rmeritz rmeritz commented Jun 4, 2020

  • Add 100% test coverage
  • Follow existing pattern of more "integration" style test where each operation that has its own test file and the file tests the in results of interacting with the fake API directly (as opposed to a true unit test that would mock out Tables's interactions with Record and Query)
  • The select, update, and create methods were already pretty extensively tested using hard coded return values in the mock API express app. I added tests for error cases using the existing "handler override" functionality. Since the "handler override" is only resets on tearDownAsync all cleanup and setup needs to be down in these test files on each test. This probably makes the test slower. I could instead add some mechanism to reset the handler after each run.

@jugglinmike
Copy link
Contributor

Taking a look now!

Copy link
Contributor

@jugglinmike jugglinmike 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, Rebecca! I especially appreciate the effort you've made to use the existing infrastructure while preserving the readability of your tests. (I can't find any online resources describing the "mystery guest" problem, though...)

Anyway, I've made a few specific suggestions. Here are some ideas for a few more tests:

  • forEach - no results are found
  • forEach - an error is thrown in the first callback
  • forEach - an error is thrown in the second callback
  • eachPage - when there are multiple pages

As a general extension to the HTTP error tests: could you verify that each statusCode you choose is being defined on the error object? That seems important for the developers who are debugging problems.

},
{typecast: true}
)
.catch(function(err) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This test will time out if the promise returned by the library is fulfilled. To make reporting a little faster and more descriptive, we could do something like...

-  .catch(function(err) {
+  .then(function() {
+      throw new Error('Promise fulfilled unexpectedly');
+    }, function(err) {
       expect(err).not.toBeNull();
-      done();
    });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That seems useful for future debugging. Done.

res.json({
id: req.params.recordId,
fields: {Name: 'Rebecca'},
createdTime: '2020-04-20T16:20:00.000Z',
Copy link
Contributor

Choose a reason for hiding this comment

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

What is createdTime? It seemed like something that we should test, but there doesn't appear to be any reference to it in the application code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is returned by the API so I thought I would include it to make the tests more realistic, but there aren't any methods provided by the library to access it. (Except through the Record#_rawJson which is clearly meant to be private.

I could see an argument to removing it because maybe it obscures the test more by providing extraneous information. But I also see an argument of leaving it in because I imagine it might be exposed in the future and it provides a realistic understanding of the format of the json returned from the API.

Copy link
Contributor

Choose a reason for hiding this comment

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

I tend toward the "extraneous information" interpretation, but I'm happy to go with your preference here, particularly because you're following precedence.


testExpressApp.set('handler override', function(req, res) {
expect(req.method).toBe('GET');
expect(req.url).toBe('/v0/app123/Table/record1?');
Copy link
Contributor

Choose a reason for hiding this comment

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

(The trailing ? is a little fragile. Ideally the library shouldn't add it, but it doesn't matter if it's there or not. There are ways we could address that, but we wouldn't need them if the library didn't do this. Probably best to accept this fragility and plan to change the library later.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm switch to match seem unideal too. I'm just going to leave it as is for now.

test/list.test.js Show resolved Hide resolved
{opts: 'arepassedalong'},
function(record) {
expect(record.getId()).toBe('recordA');
expect(record.get('Name')).toBe('Rebecca');
Copy link
Contributor

Choose a reason for hiding this comment

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

This test will pass if the callback is never invoked. Could you increment a counter in this function and then verify its value in the second callback?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Done.

fields: {Name: 'Rebecca'},
createdTime: '2020-04-20T16:20:00.000Z',
},
],
Copy link
Contributor

Choose a reason for hiding this comment

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

How about adding a second record so we can verify that the library is truly iterating?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Done.

.base('app123')
.table('Table')
.forEach(
{opts: 'arepassedalong'},
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a bit distracting for this particular test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep. Removed!

.table('Table')
.select()
.eachPage(function page(records) {
records.forEach(function(record) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This will also pass for any number of records. Could you verify records.length?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

airtable
.base('app123')
.table('Table')
.select({maxRecords: 'should not be a string'});
Copy link
Contributor

Choose a reason for hiding this comment

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

This will pass if the calls to base or table throw an error. Unlikely, sure, but still worth taking those parts of the chain outside of the assertion function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added matching of the specific error messages. I think this doesn't exactly address you point. But it does make it more clear that the errors I'm seen being throw are the ones I'm trying to throw. Is this a good enough way to address the issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that works for me

@rmeritz
Copy link
Contributor Author

rmeritz commented Jun 4, 2020

Thanks for all the feedback.

(I can't find any online resources describing the "mystery guest" problem, though...)

Btw, I think this is the original source of the term: http://xunitpatterns.com/Obscure%20Test.html#Mystery%20Guest

@rmeritz
Copy link
Contributor Author

rmeritz commented Jun 4, 2020

As a general extension to the HTTP error tests: could you verify that each statusCode you choose is being defined on the error object? That seems important for the developers who are debugging problems.

That makes sense. Done.

rmeritz added 3 commits June 4, 2020 12:47
- Add tests with more than one record
- Add test with no records
- Add iteration counter to ensure all expectations are run
@rmeritz
Copy link
Contributor Author

rmeritz commented Jun 4, 2020

I think it makes sense to skip the eachPage - when there are multiple pages test for now because that starts to dive into the details of Query internals in a serious way. I think we can cover it when we do tests for lib/query.js.

To test that errors are thrown in tests. This way if tests unexpectly
fufill the promise they immediately error instead of just timeing out.
@rmeritz
Copy link
Contributor Author

rmeritz commented Jun 5, 2020

forEach - an error is thrown in the first callback
forEach - an error is thrown in the second callback

I'm skipping adding these tests because the code doesn't handle a error be thrown in the callback at all it just bubbled up to the global event handler. I might want to change the behavior so that such an error is handled, but this is for an already deprecated function, so I don't think it makes sense to address that behavior change.

Copy link
Contributor

@jugglinmike jugglinmike left a comment

Choose a reason for hiding this comment

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

Wondrous!

Please note that in this context, my approval carries even less weight than it normally does. We'll need someone at Airtable to merge, perhaps @kasrak.

@rmeritz
Copy link
Contributor Author

rmeritz commented Jun 10, 2020

@kasrak - A lot of the test changes are interconnected, so for record tests I ended up branching of this branch. I have another branch ready for lib/records.js that I will open a PR for once this is merged: bocoup/airtable.js@table-tests-rmeritz...bocoup:record-tests-rmeritz (Probably will start something similar for query.js too, but will try and do some unrelated tests first).

Copy link
Contributor

@kasrak kasrak left a comment

Choose a reason for hiding this comment

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

Thanks!

@kasrak kasrak merged commit cb41a4f into Airtable:master Jun 10, 2020
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.

3 participants