Back to Repositories

Testing Object Utility Functions in Video.js

This test suite validates utility functions for object manipulation in Video.js, focusing on core object operations like iteration, merging, and property definition. The tests ensure reliable object handling across the Video.js codebase.

Test Coverage Overview

The test suite provides comprehensive coverage of object utility functions including:

  • Object iteration and reduction methods (each, reduce)
  • Type checking utilities (isObject, isPlain)
  • Object merging and property manipulation
  • Lazy property definition and value extraction
Edge cases include handling of empty objects, prototype properties, and various object types.

Implementation Analysis

The testing approach uses QUnit’s module and test structure with sinon for spy functionality. The implementation follows a modular pattern with isolated test cases for each utility function.

Key patterns include:
  • Custom passFail helper for type checking validation
  • Spy-based verification of iteration callbacks
  • Descriptor-level property testing for lazy evaluation

Technical Details

Testing tools and setup:
  • QUnit as the primary testing framework
  • Sinon.js for spy/stub functionality
  • ESLint integration for code quality
  • ES6 module import structure
  • Custom assertion helpers for repeated test patterns

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with clear descriptions
  • Comprehensive edge case coverage
  • Effective use of spy mechanisms for behavior verification
  • Modular test organization with nested QUnit modules
  • Consistent assertion patterns and helper functions

videojs/videoJs

test/unit/utils/obj.test.js

            
/* eslint-env qunit */
import sinon from 'sinon';
import * as Obj from '../../../src/js/utils/obj';

class Foo {
  constructor() {}
  toString() {
    return 'I am a Foo!';
  }
}

const passFail = (assert, fn, descriptor, passes, failures) => {
  Object.keys(passes).forEach(key => {
    assert.ok(fn(passes[key]), `${key} IS ${descriptor}`);
  });

  Object.keys(failures).forEach(key => {
    assert.notOk(fn(failures[key]), `${key} IS NOT ${descriptor}`);
  });
};

QUnit.module('utils/obj', function() {

  QUnit.test('each', function(assert) {
    const spy = sinon.spy();

    Obj.each({
      a: 1,
      b: 'foo',
      c: null
    }, spy);

    assert.strictEqual(spy.callCount, 3);
    assert.ok(spy.calledWith(1, 'a'));
    assert.ok(spy.calledWith('foo', 'b'));
    assert.ok(spy.calledWith(null, 'c'));

    Obj.each({}, spy);
    assert.strictEqual(spy.callCount, 3, 'an empty object was not iterated over');
  });

  QUnit.test('reduce', function(assert) {
    const first = Obj.reduce({
      a: 1,
      b: 2,
      c: 3,
      d: 4
    }, (accum, value) => accum + value);

    assert.strictEqual(first, 10);

    const second = Obj.reduce({
      a: 1,
      b: 2,
      c: 3,
      d: 4
    }, (accum, value) => accum + value, 10);

    assert.strictEqual(second, 20);

    const third = Obj.reduce({
      a: 1,
      b: 2,
      c: 3,
      d: 4
    }, (accum, value, key) => {
      accum[key] = 0 - value;
      return accum;
    }, {});

    assert.strictEqual(third.a, -1);
    assert.strictEqual(third.b, -2);
    assert.strictEqual(third.c, -3);
    assert.strictEqual(third.d, -4);
  });

  QUnit.test('isObject', function(assert) {
    passFail(assert, Obj.isObject, 'an object', {
      'plain object': {},
      'constructed object': new Foo(),
      'array': [],
      'regex': new RegExp('.'),
      'date': new Date()
    }, {
      null: null,
      function() {},
      boolean: true,
      number: 1,
      string: 'xyz'
    });
  });

  QUnit.test('isPlain', function(assert) {
    passFail(assert, Obj.isPlain, 'a plain object', {
      'plain object': {}
    }, {
      'constructed object': new Foo(),
      'null': null,
      'array': [],
      'function'() {},
      'regex': new RegExp('.'),
      'date': new Date(),
      'boolean': true,
      'number': 1,
      'string': 'xyz'
    });
  });

  QUnit.module('merge');

  QUnit.test('should merge objects', function(assert) {
    const ob1 = {
      a: true,
      b: { b1: true, b2: true, b3: true },
      c: true
    };

    const ob2 = {
      // override value
      a: false,
      // merge sub-option values
      b: { b1: true, b2: false, b4: true },
      // add new option
      d: true
    };

    const ob3 = Obj.merge(ob1, ob2);

    assert.deepEqual(ob3, {
      a: false,
      b: { b1: true, b2: false, b3: true, b4: true },
      c: true,
      d: true
    }, 'options objects merged correctly');
  });

  QUnit.test('should ignore non-objects', function(assert) {
    const obj = { a: 1 };

    assert.deepEqual(Obj.merge(obj, true), obj, 'ignored non-object input');
  });

  QUnit.module('defineLazyProperty');

  QUnit.test('should define a "lazy" property', function(assert) {
    assert.expect(12);

    const obj = {a: 1};
    const getValue = sinon.spy(() => {
      return 2;
    });

    Obj.defineLazyProperty(obj, 'b', getValue);

    let descriptor = Object.getOwnPropertyDescriptor(obj, 'b');

    assert.ok(getValue.notCalled, 'getValue function was not called');
    assert.strictEqual(typeof descriptor.get, 'function', 'descriptor has a getter');
    assert.strictEqual(typeof descriptor.set, 'function', 'descriptor has a setter');
    assert.strictEqual(typeof descriptor.value, 'undefined', 'descriptor has no value');

    let b = obj.b;

    descriptor = Object.getOwnPropertyDescriptor(obj, 'b');

    assert.ok(getValue.calledOnce, 'getValue function was not called');
    assert.strictEqual(b, 2, 'the value was retrieved correctly');
    assert.strictEqual(typeof descriptor.get, 'undefined', 'descriptor has no getter');
    assert.strictEqual(typeof descriptor.set, 'undefined', 'descriptor has no setter');
    assert.strictEqual(descriptor.value, 2, 'descriptor has a value');

    b = obj.b;
    descriptor = Object.getOwnPropertyDescriptor(obj, 'b');

    assert.ok(getValue.calledOnce, 'getValue function was still only called once');
    assert.strictEqual(b, 2, 'the value was retrieved correctly');
    assert.strictEqual(descriptor.value, 2, 'descriptor has a value');
  });

  QUnit.module('values', () => {
    QUnit.test('returns an array of values for a given object', (assert) => {
      const source = { a: 1, b: 2, c: 3 };
      const expectedResult = [1, 2, 3];

      assert.deepEqual(Obj.values(source), expectedResult, 'All values are extracted correctly');
    });

    QUnit.test('returns an empty array for an empty object', (assert) => {
      const source = {};
      const expectedResult = [];

      assert.deepEqual(Obj.values(source), expectedResult, 'Empty array is returned for an empty object');
    });

    QUnit.test('ignores prototype properties', (assert) => {
      const source = Object.create({ a: 1 });

      source.b = 2;

      const expectedResult = [2];

      assert.deepEqual(Obj.values(source), expectedResult, 'Only own properties are included in the result');
    });
  });
});