vladen/contract-decorators

View on GitHub
test.js

Summary

Maintainability
F
1 wk
Test Coverage
'option strict';

const chai = require('chai');
const sinon = require('sinon');
const sinonChai = require('sinon-chai');

chai.use(sinonChai);
const { expect } = chai;
const { spy } = sinon;

const contract = require('./');
const {
  configure, methodNameResolver, precondition, PreconditionError,
  postcondition, PostconditionError, predicateNameResolver
} = contract;

contract.enabled = true;

class CustomError extends Error {}

class Precondition {
  @precondition(argument1 => argument1, argument2 => argument2)
  method(argument1, argument2) {
    return [this, argument1, argument2];
  }
}

class Postcondition {
  @postcondition(result => result[1])
  method(argument) {
    return [this, argument];
  }
}

describe('constract', () => {
  afterEach(() => {
    configure({
      enabled: true,
      methodNameResolver, precondition, PreconditionError,
      postcondition, PostconditionError, predicateNameResolver
    });
  });

  it('Is an object', () =>
    // const contract = require('contract-decorators');
    expect(contract).to.be.an('object')
  );

  describe('configure', () => {
    it('Is a method', () =>
      // const { configure } = contract;
      expect(configure).to.be.a('function')
    );

    it('Configures enabled property', () => {
      const enabled = !contract.enabled;
      configure({ enabled  });
      expect(contract.enabled).to.equal(enabled);
    });

    it('Configures methodNameResolver property', () => {
      const config = { methodNameResolver: () => '' };
      configure(config);
      expect(contract.methodNameResolver).to.equal(config.methodNameResolver);
    });

    it('Configures PreconditionError property', () => {
      configure({ PreconditionError: CustomError  });
      expect(contract.PreconditionError).to.equal(CustomError);
    });

    it('Configures PostconditionError property', () => {
      configure({ PostconditionError: CustomError  });
      expect(contract.PostconditionError).to.equal(CustomError);
    });

    it('Configures predicateNameResolver property', () => {
      const config = { predicateNameResolver: () => '' };
      configure(config);
      expect(contract.predicateNameResolver).to.equal(config.predicateNameResolver);
    });
  });

  describe('enabled', () => {
    it('Is a property that returns boolean value', () =>
      expect(contract.enabled).to.be.a('boolean')
    );

    it('Is writable', () => {
      const enabled = contract.enabled = !contract.enabled;
      expect(contract.enabled).to.equal(enabled);
    });
  });

  describe('methodNameResolver', () => {
    it('Is a property that returns function value', () =>
      expect(contract.methodNameResolver).to.be.a('function')
    );

    it('Is writable', () => {
      const methodNameResolver = () => '';
      contract.methodNameResolver = methodNameResolver;
      expect(contract.methodNameResolver).to.equal(methodNameResolver);
    });

    it('Throws TypeError if not a function is assigned', () =>
      expect(() => contract.methodNameResolver = 0).to.throw(TypeError)
    );

    it('Calls configured methodNameResolver with argument method when contract is broken', () => {
      const method = () => {};
      contract.methodNameResolver = spy();
      const decorator = precondition(() => false);
      const descriptor = decorator({}, '', { value: method });
      try {
        descriptor.value();
      }
      catch (error) {
        expect(contract.methodNameResolver).to.be.calledWithExactly(method);
      }
    });
  });

  describe('precondition', () => {
    it('Is a method', () =>
      // const { precondition } = contract;
      expect(precondition).to.be.a('function')
    );

    it('Throws TypeError if called without arguments', () =>
      expect(() => precondition()).to.throw(TypeError)
    );

    it('Throws TypeError if one of predicates is not a function', () =>
      expect(() => precondition(() => true, 0)).to.throw(TypeError)
    );

    describe('result', () => {
      it('Is a function', () =>
        expect(precondition(() => true)).to.be.a('function')
      );

      it('Throws TypeError if not a method is being decorated', () => {
        const decorator = precondition(() => true);
        expect(() => decorator({}, '', {})).to.throw(TypeError);
      });

      it('Wraps decorated method into function with "Contract" suffix', () => {
        function test() {}
        const decorator = precondition(() => true);
        const { value } = decorator({}, 'method', { value: test });
        expect(value).to.be.a('function').and.have.property('name').that.equal('testContract');
      });

      it('Does not decorate method if contract is not enabled', () => {
        function test() {}
        contract.enabled = false;
        const decorator = precondition(() => true);
        const { value } = decorator({}, 'method', { value: test });
        expect(value).to.be.equal(test);
      });
    });

    describe('@precondition', () => {
      it('Does not throw if all arguments satisfy appropriate predicates', () => {
        /*
        class Precondition {
          @precondition(argument1 => argument1, argument2 => argument2)
          method(argument1, argument2) {
            return [this, argument1, argument2];
          }
        }
        */
        const test = new Precondition;
        expect(() => test.method(1, 2)).to.not.throw(PreconditionError);
      });

      it('Returns result returned by decorated method', () => {
        const test = new Precondition;
        expect(test.method(1, 2)).to.include.members([test, 1, 2]);
      });

      it('Throws PreconditionError if some arguments do not satisfy predicates', () => {
        // const { PreconditionError } = contract;
        const test = new Precondition;
        expect(() => test.method(1, 0)).to.throw(PreconditionError);
      });

      it('Throws custom error if it was assigned to PreconditionError and some arguments do not satisfy predicates', () => {
        contract.PreconditionError = CustomError;
        const test = new Precondition;
        expect(() => test.method(1, 0)).to.throw(CustomError);
      });

      it('Does not throw if contract is not enabled', () => {
        const test = new Precondition;
        contract.enabled = false;
        expect(() => test.method(0, 1)).to.not.throw(PreconditionError);
      });
    });
  });

  describe('PreconditionError', () => {
    it('Is a property that returns a constructor function', () =>
      expect(contract.PreconditionError)
        .to.be.a('function')
        .and.have.property('prototype').that.is.an('object')
    );

    it('Is writable', () => {
      contract.PreconditionError = CustomError;
      expect(contract.PreconditionError).to.equal(CustomError);
    });

    it('Throws TypeError if not a constructor function is assigned', () =>
      expect(() => contract.PreconditionError = () => {}).to.throw(TypeError)
    );

    it('Calls new PreconditionError with arguments method name, predicate name, method argument and argument index when contract is broken', () => {
      const argument = 1;
      function method() {}
      function predicate() { return false; }
      contract.PreconditionError = spy();
      const decorator = precondition(predicate);
      const descriptor = decorator({}, '', { value: method });
      try {
        descriptor.value(argument);
      }
      catch (error) {
        expect(contract.PreconditionError)
          .to.be.calledWithNew
          .and.calledWithExactly(method.name, predicate.name, argument, 0);
      }
    });

    it('Calls PreconditionError with arguments method name and predicate returned by custom resolvers when contract is broken', () => {
      const name = 'test';
      contract.PreconditionError = spy();
      contract.methodNameResolver = () => name;
      contract.predicateNameResolver = () => name;
      const decorator = precondition(() => false);
      const descriptor = decorator({}, '', { value: () => {} });
      try {
        descriptor.value();
        expect(null).to.not.be.ok;
      }
      catch (error) {
        expect(contract.PreconditionError).to.be.calledWith(name, name);
      }
    });
  });

  describe('postcondition', () => {
    it('Is a function', () =>
      // const { postcondition } = contract;
      expect(postcondition).to.be.a('function')
    );

    it('Throws TypeError if called without arguments', () =>
      expect(() => postcondition()).to.throw(TypeError)
    );

    it('Throws TypeError if predicate is not a function', () =>
      expect(() => postcondition(0)).to.throw(TypeError)
    );

    describe('result', () => {
      it('Is a function', () =>
        expect(postcondition(() => true)).to.be.a('function')
      );

      it('Throws TypeError if not a method is being decorated', () => {
        const decorator = postcondition(() => true);
        expect(() => decorator({}, '', {})).to.throw(TypeError);
      });

      it('Wraps decorated method into function named with "Contract" suffix', () => {
        function test() {}
        const decorator = postcondition(() => true);
        const { value } = decorator({}, 'method', { value: test });
        expect(value).to.be.a('function').and.have.property('name').that.equal('testContract');
      });

      it('Does not decorate method if contract is not enabled', () => {
        function test() {}
        contract.enabled = false;
        const decorator = postcondition(() => true);
        const { value } = decorator({}, 'method', { value: test });
        expect(value).to.be.equal(test);
      });
    });

    describe('@postcondition', () => {
      it('Returns result returned by method', () => {
        const test = new Postcondition;
        expect(test.method(1)).to.include.members([test, 1]);
      });

      it('Does not throw if result satisfies predicate', () => {
        /*
        class Postcondition {
          @postcondition(result => result[1])
          method(argument) {
            return [this, argument];
          }
        }
        */
        const test = new Postcondition;
        expect(() => test.method(1)).to.not.throw(PostconditionError);
      });

      it('Throws PostconditionError if result does not satisfy predicate', () => {
        // const { PostconditionError } = contract;
        const test = new Postcondition;
        expect(() => test.method(0)).to.throw(PostconditionError);
      });

      it('Throws custom error if it was assigned to PostconditionError and result does not satisfy predicate', () => {
        contract.PostconditionError = CustomError;
        const test = new Postcondition;
        expect(() => test.method(0)).to.throw(CustomError);
      });

      it('Does not throw if contract is not enabled', () => {
        const test = new Postcondition;
        contract.enabled = false;
        expect(() => test.method(0)).to.not.throw(PostconditionError);
      });
    });
  });

  describe('PostconditionError', () => {
    it('Is a property that returns a constructor function', () =>
      expect(contract.PostconditionError)
        .to.be.a('function')
        .and.have.property('prototype').that.is.an('object')
    );

    it('Is writable', () => {
      contract.PostconditionError = CustomError;
      expect(contract.PostconditionError).to.equal(CustomError);
    });

    it('Throws TypeError if not a constructor function is assigned', () =>
      expect(() => contract.PostconditionError = () => {}).to.throw(TypeError)
    );

    it('Calls new PostconditionError with arguments method name, predicate name and method result when contract is broken', () => {
      const result = 1;
      function method() { return result; }
      function predicate() { return false; }
      contract.PostconditionError = spy();
      const decorator = postcondition(predicate);
      const descriptor = decorator({}, '', { value: method });
      try {
        descriptor.value();
      }
      catch (error) {
        expect(contract.PostconditionError)
          .to.be.calledWithNew
          .and.calledWithExactly(method.name, predicate.name, result);
      }
    });

    it('Calls PostconditionError with arguments method name and predicate returned by custom resolvers when contract is broken', () => {
      const name = 'test';
      contract.PostconditionError = spy();
      contract.methodNameResolver = () => name;
      contract.predicateNameResolver = () => name;
      const decorator = postcondition(() => false);
      const descriptor = decorator({}, '', { value: () => {} });
      try {
        descriptor.value();
        expect(null).to.not.be.ok;
      }
      catch (error) {
        expect(contract.PostconditionError).to.be.calledWith(name, name);
      }
    });
  });

  describe('predicateNameResolver', () => {
    it('Is a property that returns a function value', () =>
      expect(contract.predicateNameResolver).to.be.a('function')
    );

    it('Is writable', () => {
      const predicateNameResolver = () => '';
      contract.predicateNameResolver = predicateNameResolver;
      expect(contract.predicateNameResolver).to.equal(predicateNameResolver);
    });

    it('Throws TypeError if not a function is assigned', () =>
      expect(() => contract.predicateNameResolver = 0).to.throw(TypeError)
    );

    it('Calls configured predicateNameResolver with argument predicate when contract is broken', () => {
      const predicate = () => false;
      contract.predicateNameResolver = spy();
      const decorator = precondition(predicate);
      const descriptor = decorator({}, '', { value: () => {} });
      try {
        descriptor.value();
      }
      catch (error) {
        expect(contract.predicateNameResolver).to.be.calledWithExactly(predicate);
      }
    });
  });
});