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

tests(repository): add missing unit tests #1383

Merged
merged 1 commit into from
Jun 20, 2018
Merged

Conversation

b-admike
Copy link
Contributor

@b-admike b-admike commented May 30, 2018

Fixes #1379. Add unit tests for the following files:

  • constraint-utils.ts
  • relation-factory.ts
  • relation-repository

Checklist

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

@b-admike b-admike self-assigned this May 30, 2018
@b-admike b-admike changed the title tests(repository): add missing unit tests [WIP]tests(repository): add missing unit tests May 30, 2018
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

Yay for adding more tests 👍

I have few suggestions on how to make the test code easier to understand and maintain, see my comments below.

it('applies a where constraint', () => {
const result = constrainFilter(originalFilter, constraint);
expect(result).to.deepEqual({
fields: {a: true},
Copy link
Member

@bajtos bajtos May 31, 2018

Choose a reason for hiding this comment

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

When reading this test in isolation, it's not clear where {a:true} is coming from.

If the test depends on specific values passed to constrainFilter, then these specific values need to be provided in the test.

Alternatively, refer to originalFilter properties in the assertion, e.g.

expect(result).to.deepEqual({
  fields: originalFilter.fields,
  limit: originalFilter.limit,
  where: Object.assign({}, originalFilter.where, {id: '5'}),
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed :) done.

undefined,
{id: '10'},
undefined,
);
Copy link
Member

Choose a reason for hiding this comment

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

Positional arguments are difficult to understand in this case - what is undefined standing for?

Please consider using a single argument containing an object with named properties.

givenAFilter({fields: undefined, where: {id: '10'}, limit: undefined});
// I think this can be further simplified to
givenAFilter({where: {id: '10'}});

Do we actually need to pass that data object through givenAFilter, considering that the input is already a valid filter and most likely the same value as will be returned by givenAFilter?

Copy link
Contributor Author

@b-admike b-admike Jun 1, 2018

Choose a reason for hiding this comment

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

I agree that using givenAFilter function is moot for the filters I use in the tests besides originalFilter. I've removed the parameter(s) and defined the filters in their respective test cases.

fields: {a: true},
limit: 5,
where: {x: 'x', id: '10'},
});
Copy link
Member

Choose a reason for hiding this comment

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

We should have only one test that's verifying whether constraintFilter changes or preserves filter properties we are not changing in the test (fields, limit, etc.).

In your current test design, if we add another property to originalFilter, let's say include, we would have to update all tests to check include too.

What to do instead: use one of the contain* matchers to verify only the property this test is interested in.

expect(result).to.containEql({
  where: /* expected where */
});

A full test case as I would write it, taking into my consideration the previous comment too.

const input = givenAFilter({where: {x: 'x'}});
const constraint = givenAWhereConstraint({id: '10'});
const result = constrainFilter(input, constraint);
expect(result).to.containEql({
  where: {x: 'x', id: '10'}
});

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 :-).

@b-admike b-admike force-pushed the tests/hasmany-relation branch 3 times, most recently from ec8e935 to bbdbd8c Compare June 1, 2018 19:46
bajtos
bajtos previously requested changes Jun 4, 2018

context('constrainDataObject', () => {
it('constrain a single data object', () => {});
it('constrain array of data objects', () => {});
Copy link
Member

Choose a reason for hiding this comment

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

Empty test cases - please implement the actual test code or remove the test case entirely.

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'm currently implementing them, but will remove for now 👍

const constraint: Filter = {where: {id: '10'}};
const result = constrainFilter(inputFilter, constraint);
expect(result).to.containEql({
where: {x: 'x', id: '10'},
Copy link
Member

Choose a reason for hiding this comment

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

In the test, it's not clear where is x: 'x' coming from - see my earlier comments.

const constraint: Filter = {where: {x: 'z'}};
const result = constrainFilter(inputFilter, constraint);
expect(result).to.containEql({
where: {and: [{x: 'x'}, {x: 'z'}]},
Copy link
Member

Choose a reason for hiding this comment

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

In the test, it's not clear where is x: 'x' coming from - see my earlier comments.

const result = constrainWhere(inputWhere, constraint);
expect(result).to.deepEqual({
x: 'x',
y: 'y',
Copy link
Member

Choose a reason for hiding this comment

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

In the test, it's not clear where x: 'x' and y: 'y' are coming from - see my earlier comments.

const constraint = {y: 'z'};
const result = constrainWhere(inputWhere, constraint);
expect(result).to.deepEqual({
and: [{x: 'x', y: 'y'}, {y: 'z'}],
Copy link
Member

Choose a reason for hiding this comment

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

ditto

@b-admike b-admike force-pushed the tests/hasmany-relation branch from bbdbd8c to 89bd236 Compare June 5, 2018 13:40
} from '../../../src/repositories/constraint-utils';

describe('constraint utility functions', () => {
let inputFilter: Filter = {};
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to assign these variables to empty objects?

let inputWhere: Where = {};

before(() => {
inputFilter = givenAFilter();
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd appreciate a comment here to let other people know what inputFilter and inputWhere looks like as objects

Copy link
Member

Choose a reason for hiding this comment

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

The purpose of a test-data builder like givenAFilter is to create any valid filter, this generic value should be used only in places where the test does not really care about what filter is used.

For tests that where the actual filter is important (e.g. because assertions make assumptions about inputs), it's important to capture the important input data directly in the test. See also my earlier comment #1383 (comment) and the example in #1383 (comment) showing how to write a test that's using these test-data builders and yet makes it clear which part of the input filter is important and where the values used in assertions are coming from.

A slightly improved example:

const input = givenAFilterWithWhere({x: 'x'});
const constraint = givenAWhereConstraint({id: '10'});
const result = constrainFilter(input, constraint);
expect(result).to.containEql({
  where: {x: 'x', id: '10'}
});

My point is that if you feel we need a comment to explain what inputFilter and inputWhere looks like to make it easier to understand what the tests are doing, then we should improve the tests instead.

@b-admike b-admike force-pushed the tests/hasmany-relation branch 2 times, most recently from 0491b09 to ae97754 Compare June 7, 2018 15:06
@jannyHou jannyHou mentioned this pull request Jun 8, 2018
2 tasks
@shimks
Copy link
Contributor

shimks commented Jun 8, 2018

EDIT: solved by importing the file with the factory function as a namespace (sinon.spy(factoryNamespace, 'hasManyRepositoryFactory')). It's weird how sinon doesn't allow you to properly spy a function just by itself. Does anyone know the justification for that?

I'm trying to set up a spy for DefaultHasManyEntityCrudRepository to write the unit tests for hasManyRepositoryFactory function and am having a hard time.

      const spy = sinon.spy(DefaultHasManyEntityCrudRepository);

      hasManyRepositoryFactory(4, meta, orderRepo);

      sinon.assert.calledWithNew(spy);
      sinon.assert.calledWith(spy, orderRepo, {customerId: 4});

This feels like it should work, but it doesn't. I've also tried sinon.spy(DefaultHasManyEntityCrudRepository.prototype, 'constructor') but that one was no good either.
I've tried googling the issue but I haven't been able to find anything useful.

Strangely enough, spy doesn't seem to work when spying a function that's being used in another. For example:

      function factory() {
        return create();
      }

      function create() {
        return {foo: 'bar'};
      }

      const spy = sinon.spy(create);
      const foobar = factory();
      expect(foobar).to.eql({foo: 'bar'});
      sinon.assert.called(spy);

The first assertion will pass, but the second will fail. Is there a gap in my knowledge with sinon as to why this shouldn't work?

@strongloop/sq-lb-apex Do you have any suggestions on what I could do?

@shimks shimks force-pushed the tests/hasmany-relation branch 2 times, most recently from 43db5be to 7940796 Compare June 11, 2018 02:59
targetRepository,
fkConstraint,
);
return new DefaultHasManyEntityCrudRepository<
Copy link
Contributor

Choose a reason for hiding this comment

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

Generics are set manually since typescript would crap out if the instance being returned was first assigned to a variable

keyFrom: 'id',
};

const spy = sinon.spy(
Copy link
Contributor

Choose a reason for hiding this comment

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

The code is in a rough state right now, but I wanted to get feedback on whether this single test case is sufficient for the unit test.

@b-admike b-admike force-pushed the tests/hasmany-relation branch from f19a286 to e69c1b4 Compare June 11, 2018 17:03
@shimks shimks force-pushed the tests/hasmany-relation branch from e69c1b4 to 572dc85 Compare June 11, 2018 17:57
const spy = sinon.spy(
RelationFactoryNamespace,
'DefaultHasManyEntityCrudRepository',
);
Copy link
Member

Choose a reason for hiding this comment

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

This is modifying global state which I find rather evil. You are not restoring DefaultHasManyEntityCrudRepository after the tests, therefore other tests we may write later will see mocked version instead of the real implementation.

If you want to test relationRepositoryFactory in isolation, then you need to find a way how to inject DefaultHasManyEntityCrudRepository constructor to the factory function. That will allow you to spy on the class locally.

const spy = sinon.spy(DefaultHasManyEntityCrudRepository);
// the spy should be used as follows:
// new spy(4, meta, orderRepo);

// This assumes that RelationFactory accepts a key/value map of relation repository classes
new RelationFactory({hasMany: spy}).createHasMany(4, meta, orderRepo);
// etc.

To be honest, I am not sure how much value there is in such unit tests. Personally, I'd look into ways how to test relationRepositoryFactory in an integration style, where we let the factory create a real repository.


I'm trying to set up a spy for DefaultHasManyEntityCrudRepository to write the unit tests for hasManyRepositoryFactory function and am having a hard time.

      const spy = sinon.spy(DefaultHasManyEntityCrudRepository);
hasManyRepositoryFactory(4, meta, orderRepo);

      sinon.assert.calledWithNew(spy);
      sinon.assert.calledWith(spy, orderRepo, {customerId: 4});

Strangely enough, spy doesn't seem to work when spying a function that's being used in another.
The first assertion will pass, but the second will fail. Is there a gap in my knowledge with sinon as to why this shouldn't work?

sinon.spy cannot rewrite function references stored in variables for you, that's a limitation of JavaScript language. Here is an equivalent of what you are trying to do:

let a = 1;
spy(a); // spy is supposed to set "a" to 2
expect(a).to.equal(2);

Copy link
Contributor

@shimks shimks Jun 12, 2018

Choose a reason for hiding this comment

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

@bajtos I'm not sure if I agree with your proposal to test this function in integration style anymore. If we were to test the factory function integration style, we'd just end up with what we have for the acceptance test right now, where we assert for products of function calls (create/find) of the constrained repository produced by the factory function. I feel this would be better covered by tests on DefaultHasManyEntityCrudRepository constructor itself.

For the sake of increasing our coverage, I thought testing whether the factory function passes the correct parameters into the relation repository constructor would somewhat fulfill the factory function's requirement to have tests.

What do you think?

Nvm, I think we can just move the acceptance test into integration and call it done :p

@b-admike b-admike force-pushed the tests/hasmany-relation branch from 4c5bffa to 8bee444 Compare June 12, 2018 15:20
@bajtos bajtos dismissed their stale review June 12, 2018 15:31

My old review is no longer relevant.

@b-admike b-admike changed the title [WIP]tests(repository): add missing unit tests tests(repository): add missing unit tests Jun 12, 2018
}
this.filter.where = constraint.where
? new WhereBuilder(this.filter.where).impose(constraint.where).build()
: new WhereBuilder(this.filter.where).impose(constraint).build();
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the rationale behind this? Do we want to allow constraint to be either a Filter or Where?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that was the use case I had in mind (if not a filter, then treat it as a where object). Thoughts?

@@ -38,18 +35,18 @@ export interface HasManyDefinition extends RelationDefinitionBase {
* relation attached to a datasource.
*
*/
export function hasManyRepositoryFactory<SourceID, T extends Entity, ID>(
export function relationRepositoryFactory<SourceID, T extends Entity, ID>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if the rename is better.

Copy link
Contributor

Choose a reason for hiding this comment

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

the rename makes sense to me since in the future we're going to be using the same factory function to produce other constrained repositories of different relations

Copy link
Member

Choose a reason for hiding this comment

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

he rename makes sense to me since in the future we're going to be using the same factory function to produce other constrained repositories of different relations

Let's defer the rename until we actually stat adding support for other relation types please.

I am not sure if it will be possible to have a single factory function, considering that each relation type needs a different repository interface.

Copy link
Contributor

Choose a reason for hiding this comment

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

I would +1 for defer the rename.
The returned value of it is still HasManyEntityCrudRepository<T>, which implies it's a factory for a specific relation

* Add a filter object. For conflicting keys with its where object,
* create an `and` clause. For any other properties, throw an error.
* @param filter filter object
* Add an object with open properties. If it is a filter object, create an `and`
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason to loose the type to be anyObject? I think we'd better not accept random constraint fields other than the ones in Filter.

Copy link
Contributor

Choose a reason for hiding this comment

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

feedback applied

@shimks shimks force-pushed the tests/hasmany-relation branch 3 times, most recently from f711ff1 to b7df3f1 Compare June 15, 2018 18:50
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

I quickly skimmed through the changes because I cannot tell what's changed since my last review and don't see any obvious problem.

The patch LGTM as far as I am concerned, but please get approval from other commenters before landing.

*/
impose(filter: Filter): this {
impose(constraint: Filter | Where): this {
Copy link
Member

Choose a reason for hiding this comment

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

if not a filter, then treat it as a where object). Thoughts?

I am little bit worried about the confusion this dual type (Filter | Where) can cause between users - I personally would not be sure which form to use when writing the code and whether a correct form is used when reading code.

Having said that, I don't have any good arguments to support my concerns, and I do see value in allowing both Filter and Where objects. I'll leave the ultimate decision on this to somebody else to make.

Copy link
Contributor Author

@b-admike b-admike Jun 18, 2018

Choose a reason for hiding this comment

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

I see your concern @bajtos, but yea it's to provide value so that when users provide a Where object for a constraint, we coerce that to a filter and provide convenience. We will document it in the TSDocs and if it confuses users like you said, we can be remove it later.

Copy link
Contributor

@jannyHou jannyHou left a comment

Choose a reason for hiding this comment

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

🎉 LGTM

add the following tests:
  - unit tests for constraint-utils.ts
  - unit tests for relation.repository.ts
  - integration tests for relation.factory.ts
moreover, refactor impose function for FilterBuilder
and applicable tests.

Co-authored-by: Kyu Shim <[email protected]>
@b-admike b-admike force-pushed the tests/hasmany-relation branch 2 times, most recently from 9231888 to 33787cc Compare June 20, 2018 04:16
@b-admike b-admike merged commit dbdcfe4 into master Jun 20, 2018
@b-admike b-admike deleted the tests/hasmany-relation branch June 20, 2018 04:29
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.

5 participants