Repository: ViacomInc/data-point
Pull request #380 (2019-08-19 15:38:06)

Add tests reducers
<!--
Thanks for your interest in the project. We appreciate bugs filed and PRs submitted!

Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).

Also, please make sure you're familiar with and follow the instructions in the
contributing guidelines (found in the CONTRIBUTING.md file).

Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!

BREAKING CHANGES:

If your PR includes a breaking change, please submit it with a codemod under
data-point-codemods that will help users upgrade their codebase.

Breaking changes without a codemod will not be accepted unless a codemod is not
viable or does not apply to the specific situation.
-->

<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**:

adds tests and applies internal refactors to codebase

<!-- Why are these changes necessary? -->
**Why**:

missing unit test:  re #357

<!-- Have you done all of these things?  -->
**Checklist**:
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
- [ ] Has Breaking changes
- [ ] Documentation
- [X] Tests
- [X] Ready to be merged <!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
- [ ] Added username to **all-contributors** list

<!-- feel free to add additional comments -->


codecov[bot]: # [Codecov](https://codecov.io/gh/ViacomInc/data-point/pull/380?src=pr&el=h1) Report
> Merging [#380](https://codecov.io/gh/ViacomInc/data-point/pull/380?src=pr&el=desc) into [canary](https://codecov.io/gh/ViacomInc/data-point/commit/cdda9462cec820d3eed7d26337514fc8c190c356?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/ViacomInc/data-point/pull/380/graphs/tree.svg?width=650&token=LYxItnrx4i&height=150&src=pr)](https://codecov.io/gh/ViacomInc/data-point/pull/380?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           canary   #380   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           3      9    +6     
  Lines          92    184   +92     
  Branches        9     28   +19     
=====================================
+ Hits           92    184   +92
```


| [Impacted Files](https://codecov.io/gh/ViacomInc/data-point/pull/380?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/data-point/src/ReducerEntity.js](https://codecov.io/gh/ViacomInc/data-point/pull/380/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvUmVkdWNlckVudGl0eS5qcw==) | `100% <100%> (ø)` | |
| [packages/data-point/src/trace-span.js](https://codecov.io/gh/ViacomInc/data-point/pull/380/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvdHJhY2Utc3Bhbi5qcw==) | `100% <100%> (ø)` | |
| [packages/data-point/src/resolve.js](https://codecov.io/gh/ViacomInc/data-point/pull/380/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvcmVzb2x2ZS5qcw==) | `100% <100%> (ø)` | |
| [packages/data-point/src/Accumulator.js](https://codecov.io/gh/ViacomInc/data-point/pull/380/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvQWNjdW11bGF0b3IuanM=) | `100% <100%> (ø)` | :arrow_up: |
| [packages/data-point/src/Reducer.js](https://codecov.io/gh/ViacomInc/data-point/pull/380/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvUmVkdWNlci5qcw==) | `100% <100%> (ø)` | |
| [packages/data-point/src/unique-id-scope.js](https://codecov.io/gh/ViacomInc/data-point/pull/380/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvdW5pcXVlLWlkLXNjb3BlLmpz) | `100% <100%> (ø)` | |
| [packages/data-point/src/reducer-symbols.js](https://codecov.io/gh/ViacomInc/data-point/pull/380/diff?src=pr&el=tree#diff-cGFja2FnZXMvZGF0YS1wb2ludC9zcmMvcmVkdWNlci1zeW1ib2xzLmpz) | `100% <0%> (ø)` | |
| ... and [2 more](https://codecov.io/gh/ViacomInc/data-point/pull/380/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ViacomInc/data-point/pull/380?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ViacomInc/data-point/pull/380?src=pr&el=footer). Last update [cdda946...0296236](https://codecov.io/gh/ViacomInc/data-point/pull/380?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/data-point/src/Accumulator.js b/packages/data-point/src/Accumulator.js
index 069a7b5..56c0c3d 100644
--- a/packages/data-point/src/Accumulator.js
+++ b/packages/data-point/src/Accumulator.js
@@ -1,13 +1,48 @@
 /**
- * class
+ * @module Accumulator
+ */
+
+/**
+ * class Accumulator
  */
 class Accumulator {
   constructor(options = {}) {
+    /**
+     * @type {any} Value being passed to the current reducer.
+     */
     this.value = options.value;
+
+    /**
+     * @type {Object} This object has properties that are accessible
+     * within the entire resolution. This is useful for providing
+     * application-level data.
+     */
     this.locals = options.locals;
-    this.cache = options.cache;
+
+    /**
+     * @type {Object} cache object, should expose `get` and `set` methods.
+     */
+    this.cache = options.cache || {};
+
+    /**
+     * @type {Span} For tracing, should comply with opentracer Span's API.
+     */
     this.tracer = options.tracer;
 
+    /**
+     * @type {Reducer} reference to the current reducer
+     */
+    this.reducer = options.reducer;
+
+    /**
+     * @type {Number} current reducer process id, this number changes on the
+     * execution of every reducer but it is not bound to a reducer.
+     */
+    this.pid = options.pid;
+
+    /**
+     * @type {function} reference to main resolver.
+     */
     Object.defineProperty(this, "__resolve", {
       writable: false,
       value: options.resolve
diff --git a/packages/data-point/src/Reducer.js b/packages/data-point/src/Reducer.js
index 6271242..a7315a6 100644
--- a/packages/data-point/src/Reducer.js
+++ b/packages/data-point/src/Reducer.js
@@ -1,18 +1,18 @@
 const { IS_REDUCER } = require("./reducer-symbols");
+const uniqueIdScope = require("./unique-id-scope");
 
-let uid = 0;
-function uniqueId() {
-  uid += 1;
-  return uid;
+const uniqueId = uniqueIdScope();
+
+function normalizeName(name) {
+  const reducerUId = uniqueId();
+  return name ? `${name}:${reducerUId}` : reducerUId;
 }
 
 class Reducer {
   constructor(type, name, spec) {
-    const normalizedName = name ? `${name}:${uniqueId()}` : uniqueId();
-
-    this.id = `${type}:${normalizedName}`;
+    this.id = `${type}:${normalizeName(name)}`;
     this.name = name;
-    this.reducerType = type;
+    this.type = type;
 
     Object.defineProperty(this, "spec", {
       value: spec,
@@ -25,16 +25,12 @@ class Reducer {
     });
   }
 
-  setAccumulatorContext(accumulator) {
-    return accumulator.set("reducer", this);
-  }
-
   async resolveReducer(accumulator, resolveReducer) {
-    const acc = this.setAccumulatorContext(accumulator);
-    return this.resolve(acc, resolveReducer);
+    return this.resolve(accumulator, resolveReducer);
   }
 }
 
 module.exports = {
+  normalizeName,
   Reducer
 };
diff --git a/packages/data-point/src/ReducerEntity.js b/packages/data-point/src/ReducerEntity.js
index 4bdce83..1732e79 100644
--- a/packages/data-point/src/ReducerEntity.js
+++ b/packages/data-point/src/ReducerEntity.js
@@ -1,45 +1,32 @@
 const { createReducer } = require("./create-reducer");
 const { Reducer } = require("./Reducer");
 
-/**
- * NOTE: this method mutates target
- * @param {String} name reducer name
- * @param {Object} target entity target
- * @param {Object} spec entity source spec
- */
-function setReducerIfTruthy(name, target, spec) {
-  if (spec[name]) {
-    // eslint-disable-next-line no-param-reassign
-    target[name] = createReducer(spec[name]);
-  }
-}
-
 class ReducerEntity extends Reducer {
   constructor(type, spec) {
     super(type, spec.name, spec);
 
-    this.isEntityInstance = true;
-
     this.uid = spec.uid;
+    this.params = spec.params || {};
 
-    setReducerIfTruthy("before", this, spec);
-    setReducerIfTruthy("value", this, spec);
-    setReducerIfTruthy("after", this, spec);
-    setReducerIfTruthy("catch", this, spec);
-
-    if (spec.inputType) {
-      // TODO: implement back
-      // const inputType = normalizeTypeCheckSource(spec.inputType);
-      this.inputType = createReducer(spec.inputType);
-    }
-
-    if (spec.outputType) {
-      // TODO: implement back
-      // const outputType = normalizeTypeCheckSource(spec.outputType);
-      this.outputType = createReducer(spec.outputType);
-    }
+    this.value = this.createReducer("value", spec);
+    this.inputType = this.createReducer("inputType", spec);
+    this.outputType = this.createReducer("outputType", spec);
+    this.before = this.createReducer("before", spec);
+    this.after = this.createReducer("after", spec);
+    this.catch = this.createReducer("catch", spec);
+  }
 
-    this.params = spec.params || {};
+  /**
+   * TODO: should we move this method up to a common class for entities and
+   * helper Reducers?
+   * NOTE: this method mutates the class
+   * @param {String} name reducer name
+   * @param {Object} target entity target
+   * @param {Object} spec entity source spec
+   */
+  // eslint-disable-next-line class-methods-use-this
+  createReducer(name, spec) {
+    return spec[name] ? createReducer(spec[name]) : undefined;
   }
 
   async resolveEntityValue(accumulator, resolveReducer) {
@@ -48,12 +35,12 @@ class ReducerEntity extends Reducer {
     acc.uid = this.uid ? this.uid(acc) : undefined;
 
     if (this.inputType) {
-      await resolveReducer(acc, this.inputValue);
+      await resolveReducer(acc, this.inputType);
     }
 
     const cache = accumulator.cache;
 
-    if (cache.get) {
+    if (typeof cache.get === "function") {
       const cacheResult = await cache.get(acc);
       if (cacheResult !== undefined) {
         return cacheResult;
@@ -80,7 +67,7 @@ class ReducerEntity extends Reducer {
       await resolveReducer(acc, this.outputType);
     }
 
-    if (cache.set) {
+    if (typeof cache.set === "function") {
       await cache.set(acc);
     }
 
@@ -88,8 +75,7 @@ class ReducerEntity extends Reducer {
   }
 
   async resolveReducer(accumulator, resolveReducer) {
-    let acc = this.setAccumulatorContext(accumulator);
-
+    let acc = accumulator;
     try {
       acc.value = await this.resolveEntityValue(acc, resolveReducer);
     } catch (error) {
@@ -111,6 +97,5 @@ class ReducerEntity extends Reducer {
 }
 
 module.exports = {
-  setReducerIfTruthy,
   ReducerEntity
 };
diff --git a/packages/data-point/src/data-point.js b/packages/data-point/src/data-point.js
index b1b7438..6dfed5d 100644
--- a/packages/data-point/src/data-point.js
+++ b/packages/data-point/src/data-point.js
@@ -1,10 +1,10 @@
 const { createReducer } = require("./create-reducer");
 const { Accumulator } = require("./Accumulator");
-const Resolve = require("./resolve-reducer");
+const { resolve } = require("./resolve");
 
 async function resolveFromAccumulator(acc, reducers) {
   const parsedReducers = createReducer(reducers);
-  return Resolve.resolve(acc, parsedReducers);
+  return resolve(acc, parsedReducers);
 }
 
 async function resolver(input, reducers, options = {}) {
diff --git a/packages/data-point/src/resolve-reducer.js b/packages/data-point/src/resolve-reducer.js
deleted file mode 100644
index 4a8e24d..0000000
--- a/packages/data-point/src/resolve-reducer.js
+++ /dev/null
@@ -1,58 +0,0 @@
-let pid = 0;
-
-/**
- * Applies a Reducer to an accumulator
- *
- * If Accumulator.trace is true it will execute tracing actions
- *
- * @param {Object} manager
- * @param {Accumulator} accumulator
- * @param {Reducer} reducer
- * @returns {Promise}
- */
-async function resolve(accumulator, reducer) {
-  pid += 1;
-
-  const acc = accumulator.create();
-  acc.reducer = reducer;
-  acc.pid = pid;
-
-  let result;
-
-  let span;
-  if (acc.tracer && acc.tracer.startSpan) {
-    const name = (acc.reducer && acc.reducer.id) || "init";
-    span = acc.tracer.startSpan(name, {
-      childOf: acc.tracer
-    });
-
-    // passing reference to accumulator to create child-parent relationship
-    acc.tracer = span;
-
-    span.setTag("pid", acc.pid);
-  }
-
-  try {
-    // NOTE: recursive call by passing resolve method
-    result = await reducer.resolveReducer(acc, resolve, reducer);
-  } catch (error) {
-    if (span && span.log) {
-      span.log({
-        event: "error",
-        "error.object": error,
-        message: error.message,
-        stack: error.stack
-      });
-    }
-    throw error;
-  } finally {
-    if (span && span.finish) {
-      span.finish();
-    }
-  }
-  return result;
-}
-
-module.exports = {
-  resolve
-};
diff --git a/packages/data-point/src/resolve.js b/packages/data-point/src/resolve.js
new file mode 100644
index 0000000..2142a83
--- /dev/null
+++ b/packages/data-point/src/resolve.js
@@ -0,0 +1,35 @@
+const uniqueIdScope = require("./unique-id-scope");
+const traceSpan = require("./trace-span");
+
+const pid = uniqueIdScope();
+
+/**
+ * Applies a Reducer to an accumulator
+ *
+ * @param {Accumulator} accumulator
+ * @param {Reducer} reducer
+ * @returns {Promise}
+ */
+async function resolve(accumulator, reducer) {
+  const acc = accumulator.create();
+  acc.reducer = reducer;
+  acc.pid = pid();
+
+  const span = traceSpan.create(acc);
+
+  let result;
+  try {
+    // NOTE: recursive call by passing resolve method
+    result = await reducer.resolveReducer(acc, resolve);
+  } catch (error) {
+    traceSpan.logError(span, error);
+    throw error;
+  } finally {
+    traceSpan.finish(span);
+  }
+  return result;
+}
+
+module.exports = {
+  resolve
+};
diff --git a/packages/data-point/src/trace-span.js b/packages/data-point/src/trace-span.js
new file mode 100644
index 0000000..aa412dc
--- /dev/null
+++ b/packages/data-point/src/trace-span.js
@@ -0,0 +1,47 @@
+/**
+ * NOTE: mutates accumulator
+ * @param {*} accumulator
+ */
+function create(accumulator) {
+  if (!accumulator.tracer || !accumulator.tracer.startSpan) {
+    return undefined;
+  }
+
+  const span = accumulator.tracer.startSpan(accumulator.reducer.id, {
+    childOf: accumulator.tracer
+  });
+
+  // passing reference to accumulator to create child-parent relationship
+  accumulator.tracer = span;
+
+  span.setTag("pid", accumulator.pid);
+
+  return span;
+}
+
+function logError(span, error) {
+  if (!span || !span.log) {
+    return;
+  }
+
+  span.log({
+    event: "error",
+    "error.object": error,
+    message: error.message,
+    stack: error.stack
+  });
+}
+
+function finish(span) {
+  if (!span || !span.finish) {
+    return;
+  }
+
+  span.finish();
+}
+
+module.exports = {
+  create,
+  logError,
+  finish
+};
diff --git a/packages/data-point/src/unique-id-scope.js b/packages/data-point/src/unique-id-scope.js
new file mode 100644
index 0000000..a5a9310
--- /dev/null
+++ b/packages/data-point/src/unique-id-scope.js
@@ -0,0 +1,9 @@
+function uniqueIdScope() {
+  let uid = 0;
+  return () => {
+    uid += 1;
+    return uid;
+  };
+}
+
+module.exports = uniqueIdScope;


Test patch:
diff --git a/packages/data-point/src/Accumulator.test.js b/packages/data-point/src/Accumulator.test.js
index 4701956..cb99ebc 100644
--- a/packages/data-point/src/Accumulator.test.js
+++ b/packages/data-point/src/Accumulator.test.js
@@ -3,25 +3,39 @@ const { Accumulator } = require("./Accumulator");
 describe("Accumulator", () => {
   describe("constructor", () => {
     it("should accept empty options object", () => {
-      expect(new Accumulator()).toEqual({});
+      expect(new Accumulator()).toMatchInlineSnapshot(`
+        Accumulator {
+          "cache": Object {},
+          "locals": undefined,
+          "pid": undefined,
+          "reducer": undefined,
+          "tracer": undefined,
+          "value": undefined,
+        }
+      `);
     });
     it("should assign options object", () => {
       const resolve = () => true;
-      expect(
-        new Accumulator({
-          value: "value",
-          locals: "locals",
-          cache: "cache",
-          tracer: "tracer",
-          resolve
-        })
-      ).toMatchObject({
+      const acc = new Accumulator({
         value: "value",
         locals: "locals",
         cache: "cache",
         tracer: "tracer",
-        __resolve: resolve
+        resolve
       });
+      expect(acc).toMatchInlineSnapshot(`
+        Accumulator {
+          "cache": "cache",
+          "locals": "locals",
+          "pid": undefined,
+          "reducer": undefined,
+          "tracer": "tracer",
+          "value": "value",
+        }
+      `);
+
+      // eslint-disable-next-line no-underscore-dangle
+      expect(acc.__resolve).toEqual(resolve);
     });
   });
 
diff --git a/packages/data-point/src/Reducer.test.js b/packages/data-point/src/Reducer.test.js
new file mode 100644
index 0000000..c369c44
--- /dev/null
+++ b/packages/data-point/src/Reducer.test.js
@@ -0,0 +1,58 @@
+jest.mock("./unique-id-scope", () => {
+  return () => () => "uniqueId";
+});
+
+const { Reducer, normalizeName } = require("./Reducer");
+const { IS_REDUCER } = require("./reducer-symbols");
+
+function createReducer() {
+  return new Reducer("type", "name", "spec");
+}
+
+describe("normalizeName", () => {
+  it("should append if provided", () => {
+    expect(normalizeName("name")).toEqual("name:uniqueId");
+  });
+
+  it("should only return unique id value if name is not provided", () => {
+    expect(normalizeName()).toEqual("uniqueId");
+  });
+});
+
+describe("Reducer", () => {
+  describe("constructor", () => {
+    it("should set type name and spec", () => {
+      const reducer = createReducer();
+      expect(reducer).toMatchObject({
+        type: "type",
+        name: "name",
+        spec: "spec"
+      });
+    });
+
+    it("should set spec as not enumerable", () => {
+      const reducer = createReducer();
+      expect(Object.prototype.propertyIsEnumerable.call(reducer, "spec")).toBe(
+        false
+      );
+    });
+
+    it("should set IS_REDUCER property as not enumerable", () => {
+      const reducer = createReducer();
+      expect(reducer[IS_REDUCER]).toBe(true);
+      expect(
+        Object.prototype.propertyIsEnumerable.call(reducer, IS_REDUCER)
+      ).toBe(false);
+    });
+  });
+
+  describe("resolveReducer", () => {
+    it("should call resolve method", async () => {
+      const reducer = createReducer();
+      reducer.resolve = jest.fn(() => "resolved");
+      const result = await reducer.resolveReducer("acc", "resolveReducer");
+      expect(reducer.resolve).toBeCalledWith("acc", "resolveReducer");
+      expect(result).toEqual("resolved");
+    });
+  });
+});
diff --git a/packages/data-point/src/ReducerEntity.test.js b/packages/data-point/src/ReducerEntity.test.js
new file mode 100644
index 0000000..d6ddd34
--- /dev/null
+++ b/packages/data-point/src/ReducerEntity.test.js
@@ -0,0 +1,464 @@
+const mockReducer = jest.fn(spec => {
+  return (value, acc) => spec(value, acc);
+});
+
+jest.mock("./create-reducer", () => {
+  return {
+    createReducer: mockReducer
+  };
+});
+
+const { ReducerEntity } = require("./ReducerEntity");
+const { Accumulator } = require("./Accumulator");
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+const mockResolveReducer = jest.fn((acc, reducer) => {
+  return reducer(acc.value, acc);
+});
+
+describe("ReducerEntity", () => {
+  describe("createReducerProperty", () => {
+    it("should return reducer if spec's property is set", () => {
+      const spec = {
+        value: () => "value"
+      };
+
+      const result = ReducerEntity.prototype.createReducer.call(
+        undefined,
+        "value",
+        spec
+      );
+
+      expect(mockReducer).toBeCalledWith(spec.value);
+      expect(result()).toEqual("value");
+    });
+
+    it("should do nothing if spec's property is not set", () => {
+      const spec = {};
+
+      const result = ReducerEntity.prototype.createReducer.call(
+        undefined,
+        "prop",
+        spec
+      );
+
+      expect(mockReducer).not.toBeCalled();
+      expect(result).toEqual(undefined);
+    });
+  });
+
+  describe("constructor", () => {
+    const uid = () => "uid";
+    const params = {
+      params: true
+    };
+    function createEntity() {
+      return new ReducerEntity("type", {
+        uid,
+        name: "customEntity",
+        value: () => "value",
+        inputType: () => "inputType",
+        outputType: () => "outputType",
+        before: () => "before",
+        after: () => "after",
+        params
+      });
+    }
+    it("should set uid", () => {
+      const entity = createEntity();
+      expect(entity).toHaveProperty("uid", uid);
+    });
+
+    it("should set params", () => {
+      const entity = createEntity();
+      expect(entity).toHaveProperty("params", params);
+    });
+
+    it("should set default params to empty object", () => {
+      const entity = new ReducerEntity("type", {});
+      expect(entity).toHaveProperty("params", {});
+    });
+
+    it("should set name", () => {
+      const entity = createEntity();
+      expect(entity).toHaveProperty("name", "customEntity");
+    });
+
+    it("should set reducers", () => {
+      const entity = createEntity();
+      expect(entity.value()).toEqual("value");
+      expect(entity.after()).toEqual("after");
+      expect(entity.before()).toEqual("before");
+      expect(entity.inputType()).toEqual("inputType");
+      expect(entity.outputType()).toEqual("outputType");
+    });
+  });
+
+  describe("resolveReducer", () => {
+    it("should resolve entityValue", async () => {
+      const entity = new ReducerEntity("type", {
+        name: "customEntity",
+        value: "value"
+      });
+
+      const mockResolveEntityValue = jest
+        .spyOn(entity, "resolveEntityValue")
+        .mockResolvedValue("entityValue");
+
+      const acc = new Accumulator();
+
+      const result = await entity.resolveReducer(acc, mockResolveReducer);
+
+      expect(mockResolveEntityValue).toBeCalledWith(acc, mockResolveReducer);
+
+      expect(result).toEqual("entityValue");
+    });
+
+    describe("handle resolveEntityValue throwing an error", () => {
+      it("should throw augmented error if this.catch is not set", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity",
+          value: () => "value"
+        });
+
+        jest
+          .spyOn(entity, "resolveEntityValue")
+          .mockRejectedValue(new Error("entityError"));
+
+        const acc = new Accumulator();
+
+        const error = await entity
+          .resolveReducer(acc, mockResolveReducer)
+          .catch(err => err);
+
+        expect(error).toMatchInlineSnapshot(`[Error: entityError]`);
+        expect(error.reducer).toEqual(entity);
+      });
+
+      it("should resolve this.catch", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity",
+          value: () => "value",
+          catch: () => "catch"
+        });
+
+        const mockResolveEntityValue = jest
+          .spyOn(entity, "resolveEntityValue")
+          .mockRejectedValue(new Error("entityError"));
+
+        const acc = new Accumulator();
+
+        const result = await entity.resolveReducer(acc, mockResolveReducer);
+
+        expect(mockResolveEntityValue).toBeCalledWith(acc, mockResolveReducer);
+        expect(mockResolveReducer).toHaveBeenCalledTimes(1);
+        expect(result).toEqual("catch");
+      });
+
+      it("should check output type of this.catch resolved value for data integrity", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity",
+          value: () => "value",
+          catch: () => "catch",
+          outputType: () => "outputType"
+        });
+
+        const mockResolveEntityValue = jest
+          .spyOn(entity, "resolveEntityValue")
+          .mockRejectedValue(new Error("entityError"));
+
+        const acc = new Accumulator();
+
+        const result = await entity.resolveReducer(acc, mockResolveReducer);
+
+        expect(mockResolveEntityValue).toBeCalledWith(acc, mockResolveReducer);
+        expect(mockResolveReducer).toHaveBeenCalledTimes(2);
+        expect(result).toEqual("catch");
+      });
+
+      it("should not handle error if catch fails", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity",
+          value: () => "value",
+          catch: () => {
+            throw new Error("catchError");
+          }
+        });
+
+        const mockResolveEntityValue = jest
+          .spyOn(entity, "resolveEntityValue")
+          .mockRejectedValue(new Error("entityError"));
+
+        const acc = new Accumulator();
+
+        const result = await entity
+          .resolveReducer(acc, mockResolveReducer)
+          .catch(err => err);
+
+        expect(mockResolveEntityValue).toBeCalledWith(acc, mockResolveReducer);
+        expect(mockResolveReducer).toHaveBeenCalledTimes(1);
+        expect(result).toMatchInlineSnapshot(`[Error: catchError]`);
+      });
+
+      it("should not handle error if outputType fails", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity",
+          value: () => "value",
+          catch: () => "catch",
+          outputType: () => {
+            throw new Error("outputTypeError");
+          }
+        });
+
+        const mockResolveEntityValue = jest
+          .spyOn(entity, "resolveEntityValue")
+          .mockRejectedValue(new Error("entityError"));
+
+        const acc = new Accumulator();
+
+        const result = await entity
+          .resolveReducer(acc, mockResolveReducer)
+          .catch(err => err);
+
+        expect(mockResolveEntityValue).toBeCalledWith(acc, mockResolveReducer);
+        expect(mockResolveReducer).toHaveBeenCalledTimes(2);
+        expect(result).toMatchInlineSnapshot(`[Error: outputTypeError]`);
+      });
+    });
+  });
+
+  describe("resolveEntityValue", () => {
+    it("should return value if no reducers are set", async () => {
+      const entity = new ReducerEntity("type", {
+        name: "customEntity"
+      });
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+      expect(result).toEqual("initialValue");
+    });
+
+    it("should assign acc.uid", async () => {
+      const entity = new ReducerEntity("type", {
+        uid: () => "uid",
+        name: "customEntity"
+      });
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      await entity.resolveEntityValue(acc, mockResolveReducer);
+      expect(acc.uid).toEqual("uid");
+    });
+
+    it("should execute inputType but not mutate result", async () => {
+      const inputType = jest.fn(() => "inputType");
+      const entity = new ReducerEntity("type", {
+        name: "customEntity",
+        inputType
+      });
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+      expect(inputType).toBeCalled();
+      expect(result).toEqual("initialValue");
+    });
+
+    describe("cache", () => {
+      it("should skip cache if cache.get is not a function", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity"
+        });
+
+        const cache = {
+          get: undefined
+        };
+
+        const acc = new Accumulator({
+          value: "initialValue",
+          cache
+        });
+
+        const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+        expect(result).toEqual("initialValue");
+      });
+
+      it("should use cache result if cache.get returns !== undefined", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity"
+        });
+
+        const cache = {
+          get: () => "cachedResult"
+        };
+
+        const acc = new Accumulator({
+          value: "initialValue",
+          cache
+        });
+
+        const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+        expect(result).toEqual("cachedResult");
+      });
+
+      it("should ignore cache result if cache.get returns undefined", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity"
+        });
+
+        const cache = {
+          get: () => undefined
+        };
+
+        const acc = new Accumulator({
+          value: "initialValue",
+          cache
+        });
+
+        const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+        expect(result).toEqual("initialValue");
+      });
+
+      it("should call cache.set if cache.set is set, and cache.get() misses", async () => {
+        const entity = new ReducerEntity("type", {
+          name: "customEntity"
+        });
+
+        const cache = {
+          get: () => undefined,
+          set: jest.fn()
+        };
+
+        const acc = new Accumulator({
+          value: "initialValue",
+          cache
+        });
+
+        const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+        expect(cache.set).toBeCalled();
+        expect(result).toEqual("initialValue");
+      });
+    });
+
+    it("should resolve and return value of this.before reducer", async () => {
+      const entity = new ReducerEntity("type", {
+        name: "customEntity",
+        before: value => `${value}:before`
+      });
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+      expect(result).toEqual("initialValue:before");
+    });
+
+    it("should resolve and return value of this.value reducer", async () => {
+      const entity = new ReducerEntity("type", {
+        name: "customEntity",
+        value: value => `${value}:value`
+      });
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+      expect(result).toEqual("initialValue:value");
+    });
+
+    it("should resolve and return value of this.after reducer", async () => {
+      const entity = new ReducerEntity("type", {
+        name: "customEntity",
+        after: value => `${value}:after`
+      });
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+      expect(result).toEqual("initialValue:after");
+    });
+
+    it("should execute outputType but not mutate result", async () => {
+      const outputType = jest.fn(() => "outputType");
+      const entity = new ReducerEntity("type", {
+        name: "customEntity",
+        outputType
+      });
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+      expect(outputType).toBeCalled();
+      expect(result).toEqual("initialValue");
+    });
+
+    it("should execute this.resolve (if set) and return value", async () => {
+      const outputType = jest.fn(() => "outputType");
+      const entity = new ReducerEntity("type", {
+        name: "customEntity",
+        outputType
+      });
+
+      entity.resolve = jest.fn(acc => `${acc.value}:resolve`);
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      const result = await entity.resolveEntityValue(acc, mockResolveReducer);
+      expect(outputType).toBeCalled();
+      expect(result).toEqual("initialValue:resolve");
+    });
+
+    it("should execute reducers in right order", async () => {
+      const tracker = [];
+      const mockResolveReducerTracker = jest.fn((acc, reducer) => {
+        tracker.push(reducer(acc.value, acc));
+      });
+
+      const entity = new ReducerEntity("type", {
+        name: "customEntity",
+        inputType: () => "inputType",
+        before: () => "before",
+        value: () => "value",
+        after: () => "after",
+        outputType: () => "outputType"
+      });
+
+      entity.resolve = () => tracker.push("resolve");
+
+      const acc = new Accumulator({
+        value: "initialValue"
+      });
+
+      await entity.resolveEntityValue(acc, mockResolveReducerTracker);
+
+      expect(tracker).toMatchInlineSnapshot(`
+        Array [
+          "inputType",
+          "before",
+          "value",
+          "resolve",
+          "after",
+          "outputType",
+        ]
+      `);
+    });
+  });
+});
diff --git a/packages/data-point/src/resolve.test.js b/packages/data-point/src/resolve.test.js
new file mode 100644
index 0000000..1564928
--- /dev/null
+++ b/packages/data-point/src/resolve.test.js
@@ -0,0 +1,87 @@
+const mockPid = jest.fn(() => "pid");
+
+jest.mock("./unique-id-scope", () => {
+  return () => mockPid;
+});
+
+jest.mock("./trace-span");
+
+const { resolve } = require("./resolve");
+const { Accumulator } = require("./Accumulator");
+const traceSpan = require("./trace-span");
+
+const reducer = {
+  resolveReducer: jest.fn(() => "resolved")
+};
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+describe("resolve", () => {
+  it("should return result of reducer.resolveReducer", async () => {
+    const acc = new Accumulator();
+    const result = await resolve(acc, reducer);
+    expect(result).toEqual("resolved");
+  });
+
+  it("should append reducer and pid to accumulator", async () => {
+    const acc = new Accumulator();
+    await resolve(acc, reducer);
+    const [accParam] = reducer.resolveReducer.mock.calls[0];
+    expect(accParam).toMatchObject({
+      pid: "pid",
+      reducer
+    });
+  });
+
+  describe("tracing", () => {
+    it("should call traceSpan.create with accumulator", async () => {
+      const acc = new Accumulator();
+      await resolve(acc, reducer);
+      const [accParam] = traceSpan.create.mock.calls[0];
+      expect(accParam).toMatchObject({
+        pid: "pid",
+        reducer
+      });
+    });
+
+    it("should call traceSpan.logError when resolveReducer throws error", async () => {
+      const acc = new Accumulator();
+
+      const reducerRejected = {
+        resolveReducer: jest
+          .fn()
+          .mockRejectedValue(new Error("resolveReducer failed"))
+      };
+
+      await resolve(acc, reducerRejected).catch(error => error);
+
+      expect(traceSpan.logError).toBeCalled();
+
+      // first argument would is an undefined span
+      expect(traceSpan.logError.mock.calls).toMatchInlineSnapshot(`
+                Array [
+                  Array [
+                    undefined,
+                    [Error: resolveReducer failed],
+                  ],
+                ]
+            `);
+    });
+
+    it("should finally call traceSpan.finish()", async () => {
+      const acc = new Accumulator();
+
+      const reducerRejected = {
+        resolveReducer: jest
+          .fn()
+          .mockRejectedValue(new Error("resolveReducer failed"))
+      };
+
+      await resolve(acc, reducerRejected).catch(error => error);
+
+      expect(traceSpan.finish).toBeCalled();
+    });
+  });
+});
diff --git a/packages/data-point/src/trace-span.test.js b/packages/data-point/src/trace-span.test.js
new file mode 100644
index 0000000..69ddd47
--- /dev/null
+++ b/packages/data-point/src/trace-span.test.js
@@ -0,0 +1,110 @@
+const traceSpan = require("./trace-span");
+const { Accumulator } = require("./Accumulator");
+
+describe("create", () => {
+  it("should return undefined if tracer is not defined", () => {
+    const acc = new Accumulator();
+    expect(traceSpan.create(acc)).toEqual(undefined);
+  });
+
+  it("should return undefined if tracer.startSpan is not defined", () => {
+    const acc = new Accumulator();
+    acc.tracer = {};
+    expect(traceSpan.create(acc)).toEqual(undefined);
+  });
+
+  describe("create span when defined", () => {
+    // NOTE: grouping a couple of expect srtatements here to keep this tests
+    // simple
+    it("should create new span", () => {
+      const acc = new Accumulator();
+
+      acc.pid = "accPId";
+
+      acc.reducer = {
+        id: "reducerId"
+      };
+
+      const span = {
+        setTag: jest.fn()
+      };
+
+      const tracer = {
+        startSpan: jest.fn(() => span)
+      };
+
+      acc.tracer = tracer;
+
+      // should return span
+      expect(traceSpan.create(acc)).toEqual(span);
+      // should set acc.tracer to new span
+      expect(acc.tracer).toEqual(span);
+
+      // tracer.startSpan should follow opentracing API
+      expect(tracer.startSpan).toBeCalled();
+      const [arg1, arg2] = tracer.startSpan.mock.calls[0];
+      expect(arg1).toEqual("reducerId");
+      // should set childOf relationship
+      expect(arg2).toMatchObject({
+        childOf: tracer
+      });
+
+      expect(span.setTag).toBeCalledWith("pid", "accPId");
+    });
+  });
+});
+
+describe("logError", () => {
+  it("should do nothing if span is not set", () => {
+    expect(() => {
+      traceSpan.logError(undefined);
+    }).not.toThrowError();
+  });
+
+  it("should do nothing if span.log is not set", () => {
+    const span = {};
+    expect(() => {
+      traceSpan.logError(span);
+    }).not.toThrowError();
+  });
+
+  it("should call span.log with error information", () => {
+    const span = {
+      log: jest.fn()
+    };
+    const error = new Error("test");
+    traceSpan.logError(span, error);
+    expect(span.log).toHaveBeenCalled();
+    const arg1 = span.log.mock.calls[0][0];
+    expect(arg1).toMatchObject({
+      event: "error",
+      "error.object": error,
+      message: "test",
+      stack: error.stack
+    });
+  });
+});
+
+describe("finish", () => {
+  it("should do nothing if span is not set", () => {
+    const span = undefined;
+    expect(() => {
+      traceSpan.finish(span);
+    }).not.toThrowError();
+  });
+
+  it("should do nothing if span.finish is not set", () => {
+    const span = {};
+    expect(() => {
+      traceSpan.finish(span);
+    }).not.toThrowError();
+  });
+
+  it("should call span.finish with error information", () => {
+    const span = {
+      finish: jest.fn()
+    };
+    traceSpan.finish(span);
+    expect(span.finish).toHaveBeenCalled();
+  });
+});
diff --git a/packages/data-point/src/unique-id-scope.test.js b/packages/data-point/src/unique-id-scope.test.js
new file mode 100644
index 0000000..5e594d2
--- /dev/null
+++ b/packages/data-point/src/unique-id-scope.test.js
@@ -0,0 +1,12 @@
+const uniqueIdScope = require("./unique-id-scope");
+
+describe("create scoped unique id generator", () => {
+  it("should generate independent unique ids per scope", () => {
+    const scope1 = uniqueIdScope();
+    const scope2 = uniqueIdScope();
+    expect(scope1()).toEqual(1);
+    expect(scope1()).toEqual(2);
+    expect(scope2()).toEqual(1);
+    expect(scope2()).toEqual(2);
+  });
+});