diff --git a/__tests__/__fixtures__/helloWorld.txt b/__tests__/__fixtures__/helloWorld.txt
new file mode 100644
index 0000000..95d09f2
--- /dev/null
+++ b/__tests__/__fixtures__/helloWorld.txt
@@ -0,0 +1 @@
+hello world
\ No newline at end of file
diff --git a/__tests__/actionUtils.test.ts b/__tests__/actionUtils.test.ts
new file mode 100644
index 0000000..4688b5d
--- /dev/null
+++ b/__tests__/actionUtils.test.ts
@@ -0,0 +1,226 @@
+import * as core from "@actions/core";
+import * as os from "os";
+import * as path from "path";
+
+import { Events, Outputs, State } from "../src/constants";
+import { ArtifactCacheEntry } from "../src/contracts";
+import * as actionUtils from "../src/utils/actionUtils";
+
+jest.mock("@actions/core");
+jest.mock("os");
+
+afterEach(() => {
+    delete process.env[Events.Key];
+});
+
+test("getArchiveFileSize returns file size", () => {
+    const filePath = path.join(__dirname, "__fixtures__", "helloWorld.txt");
+
+    const size = actionUtils.getArchiveFileSize(filePath);
+
+    expect(size).toBe(11);
+});
+
+test("isExactKeyMatch with undefined cache entry returns false", () => {
+    const key = "linux-rust";
+    const cacheEntry = undefined;
+
+    expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false);
+});
+
+test("isExactKeyMatch with empty cache entry returns false", () => {
+    const key = "linux-rust";
+    const cacheEntry: ArtifactCacheEntry = {};
+
+    expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false);
+});
+
+test("isExactKeyMatch with different keys returns false", () => {
+    const key = "linux-rust";
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: "linux-"
+    };
+
+    expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false);
+});
+
+test("isExactKeyMatch with different key accents returns false", () => {
+    const key = "linux-áccent";
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: "linux-accent"
+    };
+
+    expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false);
+});
+
+test("isExactKeyMatch with same key returns true", () => {
+    const key = "linux-rust";
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: "linux-rust"
+    };
+
+    expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(true);
+});
+
+test("isExactKeyMatch with same key and different casing returns true", () => {
+    const key = "linux-rust";
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: "LINUX-RUST"
+    };
+
+    expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(true);
+});
+
+test("setOutputAndState with undefined entry to set cache-hit output", () => {
+    const key = "linux-rust";
+    const cacheEntry = undefined;
+
+    const setOutputMock = jest.spyOn(core, "setOutput");
+    const saveStateMock = jest.spyOn(core, "saveState");
+
+    actionUtils.setOutputAndState(key, cacheEntry);
+
+    expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false");
+    expect(setOutputMock).toHaveBeenCalledTimes(1);
+
+    expect(saveStateMock).toHaveBeenCalledTimes(0);
+});
+
+test("setOutputAndState with exact match to set cache-hit output and state", () => {
+    const key = "linux-rust";
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: "linux-rust"
+    };
+
+    const setOutputMock = jest.spyOn(core, "setOutput");
+    const saveStateMock = jest.spyOn(core, "saveState");
+
+    actionUtils.setOutputAndState(key, cacheEntry);
+
+    expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "true");
+    expect(setOutputMock).toHaveBeenCalledTimes(1);
+
+    expect(saveStateMock).toHaveBeenCalledWith(
+        State.CacheResult,
+        JSON.stringify(cacheEntry)
+    );
+    expect(saveStateMock).toHaveBeenCalledTimes(1);
+});
+
+test("setOutputAndState with no exact match to set cache-hit output and state", () => {
+    const key = "linux-rust";
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: "linux-rust-bb828da54c148048dd17899ba9fda624811cfb43"
+    };
+
+    const setOutputMock = jest.spyOn(core, "setOutput");
+    const saveStateMock = jest.spyOn(core, "saveState");
+
+    actionUtils.setOutputAndState(key, cacheEntry);
+
+    expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false");
+    expect(setOutputMock).toHaveBeenCalledTimes(1);
+
+    expect(saveStateMock).toHaveBeenCalledWith(
+        State.CacheResult,
+        JSON.stringify(cacheEntry)
+    );
+    expect(saveStateMock).toHaveBeenCalledTimes(1);
+});
+
+test("getCacheState with no state returns undefined", () => {
+    const getStateMock = jest.spyOn(core, "getState");
+    getStateMock.mockImplementation(() => {
+        return "";
+    });
+
+    const state = actionUtils.getCacheState();
+
+    expect(state).toBe(undefined);
+
+    expect(getStateMock).toHaveBeenCalledWith(State.CacheResult);
+    expect(getStateMock).toHaveBeenCalledTimes(1);
+});
+
+test("getCacheState with valid state", () => {
+    const cacheEntry: ArtifactCacheEntry = {
+        cacheKey: "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43",
+        scope: "refs/heads/master",
+        creationTime: "2019-11-13T19:18:02+00:00",
+        archiveLocation: "www.actionscache.test/download"
+    };
+    const getStateMock = jest.spyOn(core, "getState");
+    getStateMock.mockImplementation(() => {
+        return JSON.stringify(cacheEntry);
+    });
+
+    const state = actionUtils.getCacheState();
+
+    expect(state).toEqual(cacheEntry);
+
+    expect(getStateMock).toHaveBeenCalledWith(State.CacheResult);
+    expect(getStateMock).toHaveBeenCalledTimes(1);
+});
+
+test("isValidEvent returns false for unknown event", () => {
+    const event = "foo";
+    process.env[Events.Key] = event;
+
+    const isValidEvent = actionUtils.isValidEvent();
+
+    expect(isValidEvent).toBe(false);
+});
+
+test("resolvePath with no ~ in path", () => {
+    const filePath = ".cache/yarn";
+
+    const resolvedPath = actionUtils.resolvePath(filePath);
+
+    const expectedPath = path.resolve(filePath);
+    expect(resolvedPath).toBe(expectedPath);
+});
+
+test("resolvePath with ~ in path", () => {
+    const filePath = "~/.cache/yarn";
+
+    const homedir = jest.requireActual("os").homedir();
+    const homedirMock = jest.spyOn(os, "homedir");
+    homedirMock.mockImplementation(() => {
+        return homedir;
+    });
+
+    const resolvedPath = actionUtils.resolvePath(filePath);
+
+    const expectedPath = path.join(homedir, ".cache/yarn");
+    expect(resolvedPath).toBe(expectedPath);
+});
+
+test("resolvePath with home not found", () => {
+    const filePath = "~/.cache/yarn";
+    const homedirMock = jest.spyOn(os, "homedir");
+    homedirMock.mockImplementation(() => {
+        return "";
+    });
+
+    expect(() => actionUtils.resolvePath(filePath)).toThrow(
+        "Unable to resolve `~` to HOME"
+    );
+});
+
+test("isValidEvent returns true for push event", () => {
+    const event = Events.Push;
+    process.env[Events.Key] = event;
+
+    const isValidEvent = actionUtils.isValidEvent();
+
+    expect(isValidEvent).toBe(true);
+});
+
+test("isValidEvent returns true for pull request event", () => {
+    const event = Events.PullRequest;
+    process.env[Events.Key] = event;
+
+    const isValidEvent = actionUtils.isValidEvent();
+
+    expect(isValidEvent).toBe(true);
+});
diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts
index f8c4cb3..d2f7dd8 100644
--- a/__tests__/restore.test.ts
+++ b/__tests__/restore.test.ts
@@ -299,7 +299,7 @@ test("restore with a pull request event and cache found", async () => {
     const cacheEntry: ArtifactCacheEntry = {
         cacheKey: key,
         scope: "refs/heads/master",
-        archiveLocation: "https://www.example.com/download"
+        archiveLocation: "www.actionscache.test/download"
     };
     const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
     getCacheMock.mockImplementation(() => {
diff --git a/src/utils/actionUtils.ts b/src/utils/actionUtils.ts
index 15b73fd..ba5b2ea 100644
--- a/src/utils/actionUtils.ts
+++ b/src/utils/actionUtils.ts
@@ -70,7 +70,11 @@ export function setOutputAndState(
 export function getCacheState(): ArtifactCacheEntry | undefined {
     const stateData = core.getState(State.CacheResult);
     core.debug(`State: ${stateData}`);
-    return (stateData && JSON.parse(stateData)) as ArtifactCacheEntry;
+    if (stateData) {
+        return JSON.parse(stateData) as ArtifactCacheEntry;
+    }
+
+    return undefined;
 }
 
 export function resolvePath(filePath: string): string {