Repository: FormidableLabs/urql
Pull request #132 (2018-11-26 16:04:46)

Maintenance/add tests to project folders
Depends on #131


Patch:
diff --git a/package.json b/package.json
index 54e1fdbe..56ea93f6 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
   "license": "MIT",
   "jest": {
     "setupFiles": [
-      "./src/tests/setup.js"
+      "./src/setupTests.ts"
     ],
     "transform": {
       "^.+\\.tsx?$": "ts-jest"
@@ -38,7 +38,7 @@
       "!src/**/hash.*",
       "!src/**/typenames.*"
     ],
-    "testRegex": "(src/tests/.*(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
+    "testRegex": "(src/.*(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
     "moduleFileExtensions": [
       "ts",
       "tsx",
diff --git a/src/components/client.tsx b/src/components/client.tsx
index 570bf1a4..206701eb 100755
--- a/src/components/client.tsx
+++ b/src/components/client.tsx
@@ -1,22 +1,20 @@
 import { Component, ReactNode } from 'react';
-import { CombinedError } from '../modules/error';
+import { CombinedError, Client } from '../lib';
 import { zipObservables } from '../utils/zip-observables';
-
 import {
   ClientEventType,
-  IEventFn,
-  IClient,
-  IExchangeResult,
-  IMutation,
-  IQuery,
-} from '../interfaces/index';
-
-export interface IClientProps {
-  client: IClient; // Client instance
+  EventFn,
+  ExchangeResult,
+  Mutation,
+  Query,
+} from '../types';
+
+export interface ClientProps {
+  client: Client; // Client instance
   children: (obj: object) => ReactNode; // Render prop
-  subscription?: IQuery; // Subscription Query object
-  query?: IQuery | IQuery[]; // Query object or array of Query objects
-  mutation?: IMutation; // Mutation object (map)
+  subscription?: Query; // Subscription Query object
+  query?: Query | Query[]; // Query object or array of Query objects
+  mutation?: Mutation; // Mutation object (map)
   updateSubscription?: (
     prev: object | null,
     next: object | null
@@ -25,18 +23,18 @@ export interface IClientProps {
   cache?: boolean;
 }
 
-export interface IClientFetchOpts {
+export interface ClientFetchOpts {
   skipCache?: boolean;
 }
 
-export interface IClientState {
+export interface ClientState {
   fetching: boolean; // Loading
   loaded: boolean; // Initial load
   error?: Error | CombinedError | CombinedError[]; // Error
-  data: object | object[] | IClientState[]; // Data
+  data: object | object[] | ClientState[]; // Data
 }
 
-export default class UrqlClient extends Component<IClientProps, IClientState> {
+export class UrqlClient extends Component<ClientProps, ClientState> {
   static defaultProps = {
     cacheInvalidation: true,
     cache: true,
@@ -139,7 +137,7 @@ export default class UrqlClient extends Component<IClientProps, IClientState> {
     }
   };
 
-  update: IEventFn = (type: ClientEventType, payload) => {
+  update: EventFn = (type: ClientEventType, payload) => {
     // This prop indicates that the Component should never update its data
     // from the cache or refetch anything
     if (!this.props.cacheInvalidation) {
@@ -162,7 +160,7 @@ export default class UrqlClient extends Component<IClientProps, IClientState> {
     }
   };
 
-  fetch = (opts: IClientFetchOpts = {}) => {
+  fetch = (opts: ClientFetchOpts = {}) => {
     const { client } = this.props;
     const skipCache = opts.skipCache || this.props.cache === false;
 
@@ -229,7 +227,7 @@ export default class UrqlClient extends Component<IClientProps, IClientState> {
             fetching: false,
           });
         },
-        next: (results: IExchangeResult[]) => {
+        next: (results: ExchangeResult[]) => {
           const errors = results.map(res => res.error).filter(Boolean);
 
           this.setState({
@@ -315,7 +313,7 @@ export default class UrqlClient extends Component<IClientProps, IClientState> {
       fetching: true,
     });
 
-    return new Promise<IExchangeResult['data']>((resolve, reject) => {
+    return new Promise<ExchangeResult['data']>((resolve, reject) => {
       // Execute mutation
       client.executeMutation$(mutation).subscribe({
         error: e => {
diff --git a/src/components/connect-hoc.tsx b/src/components/connect-hoc.tsx
index a1871598..096bf359 100755
--- a/src/components/connect-hoc.tsx
+++ b/src/components/connect-hoc.tsx
@@ -2,13 +2,13 @@ import React, { Component } from 'react';
 
 import hoistStatics from 'hoist-non-react-statics';
 
-import Connect from '../components/connect';
-import { IMutation, IQuery } from '../interfaces/index';
+import { Connect } from '../components/connect';
+import { Mutation, Query } from '../types';
 
-export interface IHOCProps {
-  query?: IQuery | IQuery[]; // Query or queries
-  subscription?: IQuery; // Subscription Query object
-  mutation?: IMutation; // Mutation map
+export interface HOCProps {
+  query?: Query | Query[]; // Query or queries
+  subscription?: Query; // Subscription Query object
+  mutation?: Mutation; // Mutation map
   updateSubscription?: (
     prev: object | null,
     next: object | null
@@ -17,7 +17,7 @@ export interface IHOCProps {
   cache?: boolean;
 }
 
-function connect(opts?: IHOCProps | ((_) => IHOCProps)) {
+function connect(opts?: HOCProps | ((_) => HOCProps)) {
   return (Comp: any) => {
     const componentName = Comp.displayName || Comp.name || 'Component';
 
diff --git a/src/components/connect.tsx b/src/components/connect.tsx
index 037ac9a2..45ea0443 100755
--- a/src/components/connect.tsx
+++ b/src/components/connect.tsx
@@ -1,13 +1,14 @@
 import React, { Component, ReactNode } from 'react';
-import { ICache, IClient, IMutation, IQuery } from '../interfaces/index';
-import ClientWrapper from './client';
-import { Consumer } from './context';
+import { Cache, Mutation, Query } from '../types';
+import { UrqlClient } from './client';
+import { ContextConsumer } from './context';
+import { Client } from '../lib';
 
-export interface IConnectProps<Data, Mutations> {
+export interface ConnectProps<Data, Mutations> {
   children: (props: UrqlProps<Data, Mutations>) => ReactNode; // Render prop
-  subscription?: IQuery;
-  query?: IQuery | IQuery[]; // Query or queries
-  mutation?: IMutation; // Mutation map
+  subscription?: Query;
+  query?: Query | Query[]; // Query or queries
+  mutation?: Mutation; // Mutation map
   updateSubscription?: (
     prev: object | null,
     next: object | null
@@ -29,7 +30,7 @@ export type UrqlProps<Data, Mutations = {}> = {
   /**
    * Urql cache
    */
-  cache: ICache;
+  cache: Cache;
   /**
    * The data returned by your Urql query.
    */
@@ -59,15 +60,15 @@ export type UrqlProps<Data, Mutations = {}> = {
   refreshAllFromCache: () => void;
 } & Mutations;
 
-export default class Connect<Data = {}, Mutations = {}> extends Component<
-  IConnectProps<Data, Mutations>
+export class Connect<Data = {}, Mutations = {}> extends Component<
+  ConnectProps<Data, Mutations>
 > {
   render() {
-    // Use react-create-context to provide context to ClientWrapper
+    // Use react-create-context to provide context to UrqlClient
     return (
-      <Consumer>
-        {(client: IClient) => (
-          <ClientWrapper
+      <ContextConsumer>
+        {(client: Client) => (
+          <UrqlClient
             client={client}
             children={this.props.children}
             subscription={this.props.subscription}
@@ -78,7 +79,7 @@ export default class Connect<Data = {}, Mutations = {}> extends Component<
             cache={this.props.cache}
           />
         )}
-      </Consumer>
+      </ContextConsumer>
     );
   }
 }
diff --git a/src/components/context.tsx b/src/components/context.tsx
index 53c48e64..47ea19dc 100644
--- a/src/components/context.tsx
+++ b/src/components/context.tsx
@@ -10,9 +10,12 @@ const context = createReactContext({});
 // --declaration emit, so we need to import ComponentClass. But if we don't
 // explicitly use ComponentClass somewhere in the code, TypeScript *also*
 // ends up issuing an error. This is dumb, but this all gets erased anyway.
-interface IContext {
+interface Context {
   Provider: ComponentClass<ProviderProps<{}>>;
   Consumer: ComponentClass<ConsumerProps<{}>>;
 }
 
-export const { Provider, Consumer }: IContext = context;
+export const {
+  Provider: ContextProvider,
+  Consumer: ContextConsumer,
+}: Context = context;
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 00000000..3c9bd84e
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,5 @@
+export * from './client';
+export * from './connect-hoc';
+export * from './connect';
+export * from './context';
+export * from './provider';
diff --git a/src/components/provider.tsx b/src/components/provider.tsx
index 0c46e230..ca69f97e 100644
--- a/src/components/provider.tsx
+++ b/src/components/provider.tsx
@@ -1,13 +1,13 @@
 import React, { Component, ReactNode } from 'react';
-import { IClient } from '../interfaces/index';
-import { Provider as ContextProvider } from './context';
+import { Client } from '../lib';
+import { ContextProvider } from './context';
 
-export interface IProviderProps {
+export interface ProviderProps {
   children: ReactNode;
-  client: IClient;
+  client: Client;
 }
 
-export default class Provider extends Component<IProviderProps> {
+export class Provider extends Component<ProviderProps> {
   render() {
     // Use react-create-context to provide client over context
     return (
diff --git a/src/modules/cache-exchange.ts b/src/exchanges/cache.ts
similarity index 83%
rename from src/modules/cache-exchange.ts
rename to src/exchanges/cache.ts
index ce4f081b..e0a475f2 100755
--- a/src/modules/cache-exchange.ts
+++ b/src/exchanges/cache.ts
@@ -1,23 +1,20 @@
 import Observable from 'zen-observable-ts';
 
-import { IClient, IExchange, IExchangeResult } from '../interfaces/index';
-import { gankTypeNamesFromResponse, formatTypeNames } from './typenames';
+import { Exchange, ExchangeResult } from '../types';
+import { Client, gankTypeNamesFromResponse, formatTypeNames } from '../lib';
 
-interface ITypenameInvalidate {
+interface TypenameInvalidate {
   [typeName: string]: string[];
 }
 
 // Wraps an exchange and refers/updates the cache according to operations
-export const cacheExchange = (
-  client: IClient,
-  forward: IExchange
-): IExchange => {
-  const typenameInvalidate: ITypenameInvalidate = {};
+export const cacheExchange = (client: Client, forward: Exchange): Exchange => {
+  const typenameInvalidate: TypenameInvalidate = {};
 
   // Fills the cache given a query's response
   const processQueryOnCache = (
     key: string,
-    response: IExchangeResult
+    response: ExchangeResult
   ): Promise<void> => {
     // Mark typenames on typenameInvalidate for early invalidation
     gankTypeNamesFromResponse(response.data).forEach(typeName => {
@@ -35,7 +32,7 @@ export const cacheExchange = (
   };
 
   // Invalidates the cache given a mutation's response
-  const processMutationOnCache = (response: IExchangeResult) => {
+  const processMutationOnCache = (response: ExchangeResult) => {
     let cacheKeys = [];
 
     // For each typeName on the response, all cache keys will need to
@@ -66,7 +63,7 @@ export const cacheExchange = (
 
     if (operationName === 'mutation') {
       // Forward mutation response but execute processMutationOnCache side-effect
-      return forwarded$.map((response: IExchangeResult) => {
+      return forwarded$.map((response: ExchangeResult) => {
         processMutationOnCache(response);
         return response;
       });
@@ -78,7 +75,7 @@ export const cacheExchange = (
     const { context, key } = operation;
     const { skipCache = false } = context;
 
-    return new Observable<IExchangeResult>(observer => {
+    return new Observable<ExchangeResult>(observer => {
       let subscription;
 
       client.cache.read(key).then(cachedResult => {
@@ -92,7 +89,7 @@ export const cacheExchange = (
         // Forward response but execute processsQueryOnCache side-effect
         subscription = forwarded$.subscribe({
           error: err => observer.error(err),
-          next: (response: IExchangeResult) => {
+          next: (response: ExchangeResult) => {
             // NOTE: Wait for cache to avoid updating a client component
             // when it itself triggered this operation
             processQueryOnCache(key, response).then(() => {
diff --git a/src/modules/dedup-exchange.ts b/src/exchanges/dedup.ts
similarity index 92%
rename from src/modules/dedup-exchange.ts
rename to src/exchanges/dedup.ts
index f133df7f..49e4256c 100644
--- a/src/modules/dedup-exchange.ts
+++ b/src/exchanges/dedup.ts
@@ -1,9 +1,9 @@
 import Observable from 'zen-observable-ts';
 
-import { IExchange } from '../interfaces/index';
+import { Exchange } from '../types';
 
 // Wraps an exchange and deduplicates in-flight operations by their key
-export const dedupExchange = (forward: IExchange): IExchange => {
+export const dedupExchange = (forward: Exchange): Exchange => {
   const inFlight = {};
 
   return operation => {
diff --git a/src/modules/http-exchange.ts b/src/exchanges/http.ts
similarity index 94%
rename from src/modules/http-exchange.ts
rename to src/exchanges/http.ts
index ed989df1..01aacac3 100755
--- a/src/modules/http-exchange.ts
+++ b/src/exchanges/http.ts
@@ -1,7 +1,7 @@
 import Observable from 'zen-observable-ts';
 
-import { IExchange } from '../interfaces/index';
-import { CombinedError } from './error';
+import { Exchange } from '../types';
+import { CombinedError } from '../lib';
 
 const checkStatus = (redirectMode: string = 'follow') => (
   response: Response
@@ -23,7 +23,7 @@ const createAbortController = () => {
   return new AbortController();
 };
 
-export const httpExchange = (): IExchange => operation => {
+export const httpExchange = (): Exchange => operation => {
   const { url, fetchOptions } = operation.context;
   const { operationName } = operation;
 
diff --git a/src/exchanges/index.ts b/src/exchanges/index.ts
new file mode 100644
index 00000000..a7d3e3ed
--- /dev/null
+++ b/src/exchanges/index.ts
@@ -0,0 +1,4 @@
+export * from './cache';
+export * from './dedup';
+export * from './http';
+export * from './subscription';
diff --git a/src/modules/subscription-exchange.ts b/src/exchanges/subscription.ts
similarity index 63%
rename from src/modules/subscription-exchange.ts
rename to src/exchanges/subscription.ts
index 747b2657..40e9b147 100755
--- a/src/modules/subscription-exchange.ts
+++ b/src/exchanges/subscription.ts
@@ -1,18 +1,22 @@
 import Observable from 'zen-observable-ts';
 
 import {
-  IExchange,
-  IExchangeResult,
-  ISubscription,
-  ISubscriptionObserver,
-} from '../interfaces/index';
+  Exchange,
+  ExchangeResult,
+  Subscription,
+  SubscriptionObserver,
+  Operation,
+} from '../types';
 
-import { CombinedError } from './error';
+import { CombinedError } from '../lib';
 
 export const subscriptionExchange = (
-  createSubscription: (IOperation, ISubscriptionObserver) => ISubscription,
-  forward: IExchange
-): IExchange => {
+  createSubscription: (
+    operation: Operation,
+    subObserver: SubscriptionObserver
+  ) => Subscription,
+  forward: Exchange
+): Exchange => {
   return operation => {
     const { operationName } = operation;
 
@@ -22,13 +26,13 @@ export const subscriptionExchange = (
     }
 
     // Take over subscription operations and call `createSubscription`
-    return new Observable<IExchangeResult>(observer => {
-      const subObserver: ISubscriptionObserver = {
+    return new Observable<ExchangeResult>(observer => {
+      const subObserver: SubscriptionObserver = {
         error: networkError => {
           observer.error(new CombinedError({ networkError }));
         },
         next: raw => {
-          const result: IExchangeResult = { operation, data: raw.data };
+          const result: ExchangeResult = { operation, data: raw.data };
           if (Array.isArray(raw.errors)) {
             result.error = new CombinedError({ graphQLErrors: raw.errors });
           }
diff --git a/src/index.ts b/src/index.ts
index 17317c1e..9501e921 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,5 @@
-export { default as Client } from './modules/client';
-export * from './modules/client';
+export { Client } from './lib/client';
+export * from './lib/client';
 
 export { default as Provider } from './components/provider';
 export * from './components/provider';
@@ -10,11 +10,11 @@ export * from './components/connect';
 export { default as ConnectHOC } from './components/connect-hoc';
 export * from './components/connect-hoc';
 
-export * from './modules/query';
-export * from './modules/cache-exchange';
-export * from './modules/dedup-exchange';
-export * from './modules/error';
-export * from './modules/http-exchange';
-export * from './modules/subscription-exchange';
+export * from './lib/query';
+export * from './exchanges/cache';
+export * from './exchanges/dedup';
+export * from './lib/error';
+export * from './exchanges/http';
+export * from './exchanges/subscription';
 
-export * from './interfaces/index';
+export * from './types';
diff --git a/src/interfaces/cache.ts b/src/interfaces/cache.ts
deleted file mode 100755
index 3773ac48..00000000
--- a/src/interfaces/cache.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export interface ICache {
-  write: (key: string, data: any) => Promise<any>;
-  read: (key: string) => Promise<any>;
-  invalidate: (key: string) => Promise<any>;
-  invalidateAll: () => Promise<any>;
-  update: (
-    callback: (store: any, key: string, value: any) => void
-  ) => Promise<any>;
-}
diff --git a/src/interfaces/client-options.ts b/src/interfaces/client-options.ts
deleted file mode 100644
index 3e5cc3e9..00000000
--- a/src/interfaces/client-options.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { ICache } from './cache';
-import { IExchange } from './exchange';
-
-export interface IClientOptions {
-  url: string;
-  fetchOptions?: object | (() => object);
-  cache?: ICache;
-  initialCache?: object;
-  transformExchange?: (IExchange, IClient) => IExchange;
-}
diff --git a/src/interfaces/client.ts b/src/interfaces/client.ts
deleted file mode 100755
index 6fc20d9c..00000000
--- a/src/interfaces/client.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import Observable from 'zen-observable-ts';
-
-import { ICache } from './cache';
-import { IEventFn } from './events';
-import { IExchangeResult } from './exchange';
-import { IQuery } from './query';
-
-export interface IClient {
-  cache: ICache;
-  cacheWithEvents: ICache;
-
-  // Event handler methods
-  dispatch: IEventFn;
-  subscribe(callback: IEventFn): () => void;
-
-  // Execute methods
-  executeSubscription$: (
-    subscriptionObject: IQuery
-  ) => Observable<IExchangeResult>;
-  executeQuery$(
-    queryObject: IQuery,
-    skipCache: boolean
-  ): Observable<IExchangeResult>;
-  executeQuery(
-    queryObject: IQuery,
-    skipCache: boolean
-  ): Promise<IExchangeResult>;
-  executeMutation$(mutationObject: IQuery): Observable<IExchangeResult['data']>;
-  executeMutation(mutationObject: IQuery): Promise<IExchangeResult['data']>;
-
-  // Batched cache operations that trigger events
-  invalidateQuery(queryObject: IQuery): Promise<void>;
-  refreshAllFromCache(): void;
-  deleteCacheKeys(keys: string[]): Promise<void>;
-  updateCacheEntry(key: string, value: any): Promise<void>;
-}
diff --git a/src/interfaces/error.ts b/src/interfaces/error.ts
deleted file mode 100644
index 5d74d20a..00000000
--- a/src/interfaces/error.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export interface IGraphQLError {
-  message?: string;
-}
diff --git a/src/interfaces/events.ts b/src/interfaces/events.ts
deleted file mode 100755
index 9c602365..00000000
--- a/src/interfaces/events.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export enum ClientEventType {
-  RefreshAll = 'RefreshAll',
-  CacheKeysInvalidated = 'CacheKeysInvalidated',
-}
-
-// NOTE: The only way to define precise strong types for events are to create
-// an interface for a function that accepts overloaded arguments
-// Each `type` argument corresponds to its call with its payload.
-// All argument lengths must match up so empty payloads must have type `void`
-// This interface is used for both `dispatch` functions and `listener` callbacks
-export interface IEventFn {
-  (type: ClientEventType.RefreshAll, payload: void): void;
-  (type: ClientEventType.CacheKeysInvalidated, payload: string[]): void;
-}
diff --git a/src/interfaces/exchange.ts b/src/interfaces/exchange.ts
deleted file mode 100755
index 60408409..00000000
--- a/src/interfaces/exchange.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import Observable from 'zen-observable-ts';
-
-import { CombinedError } from '../modules/error';
-import { IOperation } from './operation';
-
-// Adapted from: https://github.com/graphql/graphql-js/blob/ae5b163d2e6c124107fa0971f6d838c8a7d29f51/src/execution/execute.js#L105-L114<Paste>
-export interface IExecutionResult {
-  errors?: Error[];
-  data?: object;
-}
-
-export interface IExchangeResult {
-  operation: IOperation; // Add on the original operation
-  data: IExecutionResult['data'];
-  error?: CombinedError;
-}
-
-export type IExchange = (operation: IOperation) => Observable<IExchangeResult>;
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
deleted file mode 100644
index e810b9b6..00000000
--- a/src/interfaces/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export { IClient } from './client';
-export { IMutation } from './mutation';
-export { IQuery } from './query';
-export { IClientOptions } from './client-options';
-export { ICache } from './cache';
-export { IExchange, IExchangeResult } from './exchange';
-export { IGraphQLError } from './error';
-export { ClientEventType, IEventFn } from './events';
-export { IOperation } from './operation';
-export { ISubscriptionObserver, ISubscription } from './subscription';
diff --git a/src/interfaces/mutation.ts b/src/interfaces/mutation.ts
deleted file mode 100644
index 4d83d83e..00000000
--- a/src/interfaces/mutation.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { IQuery } from './query';
-
-export interface IMutation {
-  [key: string]: IQuery;
-}
diff --git a/src/interfaces/operation.ts b/src/interfaces/operation.ts
deleted file mode 100644
index 4cd01722..00000000
--- a/src/interfaces/operation.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { IQuery } from './query';
-
-/* This is an "enriched" Query which makes it ready to
- * go through an OperationQueue. It contains a context
- * that will be relevant to the Exchange.
- * - `key`: The unique identifier of the Query
- * - `operationName`: The GraphQL operation like "query" or "mutation"
- * - `context`: Any options that the Exchange should receive
- */
-export interface IOperation extends IQuery {
-  key: string;
-  operationName: string;
-  context: Record<string, any>;
-}
diff --git a/src/interfaces/query.ts b/src/interfaces/query.ts
deleted file mode 100755
index 03a97800..00000000
--- a/src/interfaces/query.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export interface IQuery {
-  query: string;
-  variables?: object;
-}
diff --git a/src/interfaces/subscription.ts b/src/interfaces/subscription.ts
deleted file mode 100644
index d59db8a0..00000000
--- a/src/interfaces/subscription.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// These are *not* Observable primitives, but are instead intended to
-// be used with the subscriptionExchange
-
-export interface ISubscriptionObserver {
-  next: (IExecutionResult) => void;
-  error: (Error) => void;
-}
-
-export interface ISubscription {
-  unsubscribe: () => void;
-}
diff --git a/src/modules/client.ts b/src/modules/client.ts
deleted file mode 100755
index 65fc23f2..00000000
--- a/src/modules/client.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-import Observable from 'zen-observable-ts';
-
-import {
-  ClientEventType,
-  IEventFn,
-  ICache,
-  IClient,
-  IClientOptions,
-  IExchange,
-  IExchangeResult,
-  IQuery,
-} from '../interfaces/index';
-
-import { cacheExchange } from './cache-exchange';
-import { dedupExchange } from './dedup-exchange';
-import { defaultCache } from './default-cache';
-import { hashString } from './hash';
-import { httpExchange } from './http-exchange';
-
-const getQueryKey = (q: IQuery) => {
-  const { query, variables } = q;
-  return hashString(JSON.stringify({ query, variables }));
-};
-
-export default class Client implements IClient {
-  url: string;
-  store: object; // Internal store
-  subscriptionSize: number; // Used to generate IDs for subscriptions
-  cache: ICache; // Cache object
-  exchange: IExchange; // Exchange to communicate with GraphQL APIs
-  fetchOptions: RequestInit | (() => RequestInit); // Options for fetch call
-  subscriptions: { [id: string]: IEventFn }; // Map of subscribed Connect components
-
-  constructor(opts?: IClientOptions) {
-    if (!opts) {
-      throw new Error('Please provide configuration object');
-    } else if (!opts.url) {
-      throw new Error('Please provide a URL for your GraphQL API');
-    }
-
-    this.url = opts.url;
-    this.store = opts.initialCache || {};
-    this.subscriptions = Object.create(null);
-    this.subscriptionSize = 0;
-    this.cache = opts.cache || defaultCache(this.store);
-    this.fetchOptions = opts.fetchOptions || {};
-
-    const exchange = cacheExchange(this, dedupExchange(httpExchange()));
-    this.exchange = opts.transformExchange
-      ? opts.transformExchange(exchange, this)
-      : exchange;
-  }
-
-  /* Event handler methods: */
-
-  dispatch: IEventFn = (type, payload) => {
-    /* tslint:disable-next-line forin */
-    for (const sub in this.subscriptions) {
-      this.subscriptions[sub](type, payload);
-    }
-  };
-
-  subscribe(callback: IEventFn): () => void {
-    // Create an identifier, add callback to subscriptions
-    const id = this.subscriptionSize++;
-    this.subscriptions[id] = callback;
-
-    // Return unsubscription function
-    return () => {
-      delete this.subscriptions[id];
-    };
-  }
-
-  /* Cache methods: */
-
-  // Receives keys and invalidates them on the cache
-  // Dispatches a CacheKeysInvalidated event after
-  deleteCacheKeys = (keys: string[]): Promise<void> => {
-    const batchedInvalidate = keys.map(key => this.cache.invalidate(key));
-
-    return Promise.all(batchedInvalidate).then(() => {
-      this.dispatch(ClientEventType.CacheKeysInvalidated, keys);
-    });
-  };
-
-  // Receives [key, value] entry and writes it to the cache
-  // Dispatches a CacheEntryUpdated event after
-  updateCacheEntry = (key: string, value: any): Promise<void> => {
-    return this.cache.write(key, value).then(() => {
-      this.dispatch(ClientEventType.CacheKeysInvalidated, [key]);
-    });
-  };
-
-  refreshAllFromCache = () => {
-    this.dispatch(ClientEventType.RefreshAll, undefined);
-  };
-
-  invalidateQuery = (queryObject: IQuery) => {
-    const key = getQueryKey(queryObject);
-    return this.cache.invalidate(key).then(() => {
-      this.dispatch(ClientEventType.CacheKeysInvalidated, [key]);
-    });
-  };
-
-  // Cache methods that are exposed on the component and will dispatch their
-  // changes
-  cacheWithEvents: ICache = {
-    write: this.updateCacheEntry,
-    read: (key: string) => this.cache.read(key),
-    invalidate: (key: string) => this.deleteCacheKeys([key]),
-    invalidateAll: () =>
-      this.cache.invalidateAll().then(() => {
-        this.dispatch(ClientEventType.RefreshAll, undefined);
-      }),
-    update: (callback: (...args: any[]) => void) =>
-      this.cache.update(callback).then(() => {
-        this.dispatch(ClientEventType.RefreshAll, undefined);
-      }),
-  };
-
-  /* Execute methods: */
-
-  makeContext({ skipCache }: { skipCache?: boolean }): Record<string, any> {
-    return {
-      fetchOptions:
-        typeof this.fetchOptions === 'function'
-          ? this.fetchOptions()
-          : this.fetchOptions,
-      skipCache: !!skipCache,
-      url: this.url,
-    };
-  }
-
-  executeSubscription$(
-    subscriptionObject: IQuery
-  ): Observable<IExchangeResult> {
-    const { query, variables } = subscriptionObject;
-
-    const operation = {
-      context: this.makeContext({}),
-      key: getQueryKey(subscriptionObject),
-      operationName: 'subscription',
-      query,
-      variables,
-    };
-
-    return this.exchange(operation);
-  }
-
-  executeQuery$(
-    queryObject: IQuery,
-    skipCache: boolean
-  ): Observable<IExchangeResult> {
-    const { query, variables } = queryObject;
-
-    const operation = {
-      context: this.makeContext({ skipCache }),
-      key: getQueryKey(queryObject),
-      operationName: 'query',
-      query,
-      variables,
-    };
-
-    return this.exchange(operation);
-  }
-
-  executeQuery = (
-    queryObject: IQuery,
-    skipCache: boolean
-  ): Promise<IExchangeResult> => {
-    return new Promise<IExchangeResult>((resolve, reject) => {
-      this.executeQuery$(queryObject, skipCache).subscribe({
-        error: reject,
-        next: resolve,
-      });
-    });
-  };
-
-  executeMutation$(
-    mutationObject: IQuery
-  ): Observable<IExchangeResult['data']> {
-    const { query, variables } = mutationObject;
-
-    const operation = {
-      context: this.makeContext({}),
-      key: getQueryKey(mutationObject),
-      operationName: 'mutation',
-      query,
-      variables,
-    };
-
-    return this.exchange(operation).map(x => x.data);
-  }
-
-  executeMutation = (
-    mutationObject: IQuery
-  ): Promise<IExchangeResult['data']> => {
-    return new Promise<IExchangeResult>((resolve, reject) => {
-      this.executeMutation$(mutationObject).subscribe({
-        error: reject,
-        next: resolve,
-      });
-    });
-  };
-}
diff --git a/src/modules/default-cache.ts b/src/modules/default-cache.ts
deleted file mode 100644
index 9d3696b2..00000000
--- a/src/modules/default-cache.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ICache } from '../interfaces/index';
-
-export const defaultCache = (store: object): ICache => ({
-  invalidate: hash =>
-    new Promise(resolve => {
-      delete store[hash];
-      resolve(hash);
-    }),
-  invalidateAll: () =>
-    new Promise(resolve => {
-      store = {};
-      resolve();
-    }),
-  read: hash =>
-    new Promise(resolve => {
-      resolve(store[hash] || null);
-    }),
-  update: callback =>
-    new Promise(resolve => {
-      if (typeof callback === 'function') {
-        Object.keys(store).forEach(key => {
-          callback(store, key, store[key]);
-        });
-      }
-      resolve();
-    }),
-  write: (hash, data) =>
-    new Promise(resolve => {
-      store[hash] = data;
-      resolve(hash);
-    }),
-});
diff --git a/src/modules/error.ts b/src/modules/error.ts
deleted file mode 100644
index 7998373d..00000000
--- a/src/modules/error.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { IGraphQLError } from '../interfaces/index';
-
-const generateErrorMessage = (
-  networkErr?: Error,
-  graphQlErrs?: IGraphQLError[]
-) => {
-  let error = '';
-  if (networkErr) {
-    error = `[Network] ${networkErr.message}`;
-    return error;
-  }
-
-  graphQlErrs.forEach(err => {
-    error += `[GraphQL] ${err.message}\n`;
-  });
-
-  return error.trim();
-};
-
-const rehydrateGraphQlError = (error: string | IGraphQLError): Error => {
-  if (typeof error === 'string') {
-    return new Error(error);
-  } else if (error.message) {
-    return new Error(error.message);
-  } else {
-    return error as any;
-  }
-};
-
-export class CombinedError implements Error {
-  public name: string;
-  public message: string;
-  public graphQLErrors: Error[];
-  public networkError?: Error;
-  public response?: any;
-
-  constructor({
-    networkError,
-    graphQLErrors,
-    response,
-  }: {
-    networkError?: Error;
-    graphQLErrors?: Array<string | IGraphQLError>;
-    response?: any;
-  }) {
-    this.name = 'CombinedError';
-    this.graphQLErrors = (graphQLErrors || []).map(rehydrateGraphQlError);
-    this.message = generateErrorMessage(networkError, this.graphQLErrors);
-    this.networkError = networkError;
-    this.response = response;
-  }
-}
diff --git a/src/modules/hash.ts b/src/modules/hash.ts
deleted file mode 100644
index 3c28f897..00000000
--- a/src/modules/hash.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/* tslint:disable */
-
-export function hashString(str: string) {
-  return hash(str).toString(36);
-}
-
-// Source: https://github.com/garycourt/murmurhash-js/blob/master/murmurhash2_gc.js
-function hash(str: string) {
-  var l = str.length,
-    h = l ^ l,
-    i = 0,
-    k;
-
-  while (l >= 4) {
-    k =
-      (str.charCodeAt(i) & 0xff) |
-      ((str.charCodeAt(++i) & 0xff) << 8) |
-      ((str.charCodeAt(++i) & 0xff) << 16) |
-      ((str.charCodeAt(++i) & 0xff) << 24);
-
-    k =
-      (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16);
-    k ^= k >>> 24;
-    k =
-      (k & 0xffff) * 0x5bd1e995 + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16);
-
-    h =
-      ((h & 0xffff) * 0x5bd1e995 +
-        ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^
-      k;
-
-    l -= 4;
-    ++i;
-  }
-
-  switch (l) {
-    case 3:
-      h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
-    case 2:
-      h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
-    case 1:
-      h ^= str.charCodeAt(i) & 0xff;
-      h =
-        (h & 0xffff) * 0x5bd1e995 +
-        ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16);
-  }
-
-  h ^= h >>> 13;
-  h = (h & 0xffff) * 0x5bd1e995 + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16);
-  h ^= h >>> 15;
-
-  return h >>> 0;
-}
diff --git a/src/modules/query.ts b/src/modules/query.ts
deleted file mode 100644
index 28b1e8d7..00000000
--- a/src/modules/query.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-const createQuery = (q: string, vars?: object) => {
-  return {
-    query: q,
-    variables: vars || {},
-  };
-};
-
-export const query = createQuery;
-export const mutation = createQuery;
-export const subscription = createQuery;
diff --git a/src/modules/typenames.ts b/src/modules/typenames.ts
deleted file mode 100755
index b76235f4..00000000
--- a/src/modules/typenames.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import {
-  DefinitionNode,
-  DocumentNode,
-  FieldNode,
-  OperationDefinitionNode,
-  SelectionSetNode,
-} from 'graphql';
-
-import { parse } from 'graphql/language/parser';
-import { print } from 'graphql/language/printer';
-
-const TYPENAME_FIELD: FieldNode = {
-  kind: 'Field',
-  name: {
-    kind: 'Name',
-    value: '__typename',
-  },
-};
-
-function addTypename(selectionSet: SelectionSetNode, isRoot = false) {
-  if (selectionSet.selections) {
-    if (!isRoot) {
-      const exists = selectionSet.selections.some(selection => {
-        return (
-          selection.kind === 'Field' &&
-          (selection as FieldNode).name.value === '__typename'
-        );
-      });
-
-      if (!exists) {
-        selectionSet.selections.push(TYPENAME_FIELD);
-      }
-    }
-
-    selectionSet.selections.forEach(selection => {
-      if (selection.kind === 'Field') {
-        if (
-          selection.name.value.lastIndexOf('__', 0) !== 0 &&
-          selection.selectionSet
-        ) {
-          addTypename(selection.selectionSet);
-        }
-      } else if (selection.kind === 'InlineFragment') {
-        if (selection.selectionSet) {
-          addTypename(selection.selectionSet);
-        }
-      }
-    });
-  }
-}
-
-function addTypenameToDocument(doc: DocumentNode) {
-  doc.definitions.forEach((definition: DefinitionNode) => {
-    const isRoot = definition.kind === 'OperationDefinition';
-    addTypename((definition as OperationDefinitionNode).selectionSet, isRoot);
-  });
-
-  return doc;
-}
-
-// Adds __typename fields to a GraphQL query string
-export function formatTypeNames(query: string): string {
-  const doc = parse(query);
-  return print(addTypenameToDocument(doc));
-}
-
-function getTypeNameFromField(obj: object, typenames: string[]) {
-  Object.keys(obj).map(item => {
-    if (typeof obj[item] === 'object') {
-      if (obj[item] && '__typename' in obj[item]) {
-        typenames.push(obj[item].__typename);
-      }
-      if (obj[item]) {
-        getTypeNameFromField(obj[item], typenames);
-      }
-    }
-  });
-}
-
-export function gankTypeNamesFromResponse(response: object) {
-  const typeNames = [];
-  getTypeNameFromField(response, typeNames);
-  return typeNames.filter((v, i, a) => a.indexOf(v) === i);
-}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 00000000..df421bb0
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,72 @@
+import Observable from 'zen-observable-ts';
+import { Client, CombinedError } from '../lib';
+
+export interface Cache {
+  write: (key: string, data: any) => Promise<any>;
+  read: (key: string) => Promise<any>;
+  invalidate: (key: string) => Promise<any>;
+  invalidateAll: () => Promise<any>;
+  update: (
+    callback: (store: any, key: string, value: any) => void
+  ) => Promise<any>;
+}
+
+export interface ClientOptions {
+  url: string;
+  fetchOptions?: object | (() => object);
+  cache?: Cache;
+  initialCache?: object;
+  transformExchange?: (exchange: Exchange, client: Client) => Exchange;
+}
+
+export interface GraphQLError {
+  message?: string;
+}
+
+export enum ClientEventType {
+  RefreshAll = 'RefreshAll',
+  CacheKeysInvalidated = 'CacheKeysInvalidated',
+}
+
+export interface EventFn {
+  (type: ClientEventType.RefreshAll, payload: void): void;
+  (type: ClientEventType.CacheKeysInvalidated, payload: string[]): void;
+}
+
+// Adapted from: https://github.com/graphql/graphql-js/blob/ae5b163d2e6c124107fa0971f6d838c8a7d29f51/src/execution/execute.js#L105-L114<Paste>
+export interface ExecutionResult {
+  errors?: Error[];
+  data?: object;
+}
+
+export interface ExchangeResult {
+  operation: Operation; // Add on the original operation
+  data: ExecutionResult['data'];
+  error?: CombinedError;
+}
+
+export type Exchange = (operation: Operation) => Observable<ExchangeResult>;
+
+export interface Mutation {
+  [key: string]: Query;
+}
+
+export interface Operation extends Query {
+  key: string;
+  operationName: string;
+  context: Record<string, any>;
+}
+
+export interface Query {
+  query: string;
+  variables?: object;
+}
+
+export interface SubscriptionObserver {
+  next: (IExecutionResult) => void;
+  error: (Error) => void;
+}
+
+export interface Subscription {
+  unsubscribe: () => void;
+}
diff --git a/tsconfig.json b/tsconfig.json
index c5888811..63be720c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,6 +20,7 @@
     "src/**/*.spec.ts",
     "src/**/*.spec.tsx",
     "src/**/*.test.ts",
-    "src/**/*.test.tsx"
+    "src/**/*.test.tsx",
+    "src/setupTests.ts"
   ]
 }
diff --git a/tslint.json b/tslint.json
index 62922bec..054503f6 100755
--- a/tslint.json
+++ b/tslint.json
@@ -8,6 +8,7 @@
     "member-ordering": false,
     "ordered-imports": false,
     "member-access": false,
-    "no-submodule-imports": false
+    "no-submodule-imports": false,
+    "interface-name": false
   }
 }


Test patch:
diff --git a/src/tests/components/__snapshots__/client.test.tsx.snap b/src/components/__snapshots__/client.test.tsx.snap
similarity index 100%
rename from src/tests/components/__snapshots__/client.test.tsx.snap
rename to src/components/__snapshots__/client.test.tsx.snap
diff --git a/src/tests/components/__snapshots__/connect-hoc.test.tsx.snap b/src/components/__snapshots__/connect-hoc.test.tsx.snap
similarity index 100%
rename from src/tests/components/__snapshots__/connect-hoc.test.tsx.snap
rename to src/components/__snapshots__/connect-hoc.test.tsx.snap
diff --git a/src/tests/components/__snapshots__/connect.test.tsx.snap b/src/components/__snapshots__/connect.test.tsx.snap
similarity index 100%
rename from src/tests/components/__snapshots__/connect.test.tsx.snap
rename to src/components/__snapshots__/connect.test.tsx.snap
diff --git a/src/tests/components/__snapshots__/provider.test.tsx.snap b/src/components/__snapshots__/provider.test.tsx.snap
similarity index 100%
rename from src/tests/components/__snapshots__/provider.test.tsx.snap
rename to src/components/__snapshots__/provider.test.tsx.snap
diff --git a/src/tests/components/client.test.tsx b/src/components/client.test.tsx
similarity index 93%
rename from src/tests/components/client.test.tsx
rename to src/components/client.test.tsx
index 6ce6e1f5..bfed6998 100755
--- a/src/tests/components/client.test.tsx
+++ b/src/components/client.test.tsx
@@ -1,11 +1,10 @@
 /* tslint:disable */
 
 import React from 'react';
-import Client from '../../components/client';
-import { CombinedError } from '../../modules/error';
-import { default as ClientModule } from '../../modules/client';
-import { subscriptionExchange } from '../../modules/subscription-exchange';
-import { ClientEventType } from '../../interfaces/index';
+import { UrqlClient } from '.';
+import { Client, CombinedError } from '../lib';
+import { subscriptionExchange } from '../exchanges';
+import { ClientEventType } from '../types';
 import renderer from 'react-test-renderer';
 
 describe('Client Component', () => {
@@ -17,13 +16,12 @@ describe('Client Component', () => {
 
   it('should return null with no render function', () => {
     // @ts-ignore
-    const component = renderer.create(<Client />);
+    const component = renderer.create(<UrqlClient />);
     let tree = component.toJSON();
     expect(tree).toBeNull();
     component.update(
       // @ts-ignore
-      <Client
-        // @ts-ignore
+      <UrqlClient
         children={() => {
           return <div>hey</div>;
         }}
@@ -36,7 +34,7 @@ describe('Client Component', () => {
   it('should use the render prop when supplied and render the defaults', done => {
     renderer.create(
       // @ts-ignore
-      <Client
+      <UrqlClient
         // @ts-ignore
         children={({ data, error, fetching, loaded, refetch }) => {
           expect(data).toBeNull();
@@ -58,11 +56,11 @@ describe('Client Component', () => {
         json: () => ({ data: { todos: [{ id: 1 }] } }),
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         client={clientModule}
         // @ts-ignore
         query={{ query: `{ todos { id } }` }}
@@ -99,11 +97,11 @@ describe('Client Component', () => {
         json: () => ({ data: { todos: [{ id: 1 }] } }),
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
 
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         key="test"
         client={clientModule}
         // @ts-ignore
@@ -115,7 +113,7 @@ describe('Client Component', () => {
     const newQuery = { query: `{ posts { id } }` };
 
     client.update(
-      <Client
+      <UrqlClient
         key="test"
         client={clientModule}
         // @ts-ignore
@@ -137,12 +135,12 @@ describe('Client Component', () => {
         json: () => ({ data: { todos: [{ id: 1 }] } }),
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     // @ts-ignore
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         // @ts-ignore
         key="test"
         client={clientModule}
@@ -163,7 +161,7 @@ describe('Client Component', () => {
     ]);
 
     client.update(
-      <Client
+      <UrqlClient
         // @ts-ignore
         key="test"
         client={clientModule}
@@ -186,11 +184,11 @@ describe('Client Component', () => {
 
   it('should return an error thrown by fetch', done => {
     (global as any).fetch.mockReturnValue(Promise.reject(new Error('oh no!')));
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         client={clientModule}
         // @ts-ignore
         query={{ query: `{ todos { id } }` }}
@@ -227,11 +225,11 @@ describe('Client Component', () => {
         json: () => ({ data: { todos: [{ id: 1, __typename: 'Todo' }] } }),
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         client={clientModule}
         // @ts-ignore
         query={[{ query: `{ todos { id } }` }, { query: `{ todos { id } }` }]}
@@ -266,11 +264,11 @@ describe('Client Component', () => {
 
   it('should return the proper render prop arguments with multiple queries supplied and an error', done => {
     (global as any).fetch.mockReturnValue(Promise.reject(new Error('lol')));
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         client={clientModule}
         // @ts-ignore
         query={[{ query: `{ todos { id } }` }, { query: `{ todos { id } }` }]}
@@ -299,11 +297,11 @@ describe('Client Component', () => {
         data: { todos: [{ id: 1 }] },
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         // @ts-ignore
         client={clientModule}
         // @ts-ignore
@@ -349,13 +347,13 @@ describe('Client Component', () => {
         json: () => ({ data: { todos: [{ id: 1, __typename: 'Todo' }] } }),
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const spy = jest.spyOn(global, 'fetch');
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         // @ts-ignore
         client={clientModule}
         // @ts-ignore
@@ -406,11 +404,11 @@ describe('Client Component', () => {
         }),
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         // @ts-ignore
         client={clientModule}
         // @ts-ignore
@@ -452,12 +450,12 @@ describe('Client Component', () => {
         data: { todos: [{ id: 1, __typename: 'Todo' }] },
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     // @ts-ignore
     const spy = jest.spyOn(global, 'fetch');
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         // @ts-ignore
         client={clientModule}
         // @ts-ignore
@@ -488,12 +486,12 @@ describe('Client Component', () => {
         data: { todos: [{ id: 1, __typename: 'Todo' }] },
       })
     );
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     // @ts-ignore
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         // @ts-ignore
         client={clientModule}
         // @ts-ignore
@@ -535,11 +533,11 @@ describe('Client Component', () => {
   it('should not update in response to mutations that throw', done => {
     (global as any).fetch.mockReturnValue(Promise.reject(new Error('Yoinks!')));
 
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         // @ts-ignore
         client={clientModule}
         // @ts-ignore
@@ -572,11 +570,11 @@ describe('Client Component', () => {
   });
 
   it('shouldnt return data or mutations if neither is provided', () => {
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         // @ts-ignore
         client={clientModule}
         // @ts-ignore
@@ -839,7 +837,7 @@ describe('Client Component', () => {
       return { unsubscribe };
     };
 
-    const clientModule = new ClientModule({
+    const clientModule = new Client({
       url: 'test',
       transformExchange: x => subscriptionExchange(createSubscription, x),
     });
@@ -847,7 +845,7 @@ describe('Client Component', () => {
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         client={clientModule}
         subscription={{ query: `subscription { todos { id } }` }}
         children={args => {
@@ -894,7 +892,7 @@ describe('Client Component', () => {
       };
 
       // @ts-ignore
-      const c = new Client(p as any);
+      const c = new UrqlClient(p as any);
 
       c.formatProps(p);
     }).toThrowErrorMatchingSnapshot();
@@ -918,7 +916,7 @@ describe('Client Component', () => {
       return { unsubscribe };
     };
 
-    const clientModule = new ClientModule({
+    const clientModule = new Client({
       url: 'test',
       transformExchange: x => subscriptionExchange(createSubscription, x),
     });
@@ -929,7 +927,7 @@ describe('Client Component', () => {
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         client={clientModule}
         query={{ query }}
         subscription={{ query: `subscription { todos { id } }` }}
@@ -979,7 +977,7 @@ describe('Client Component', () => {
       return { unsubscribe };
     };
 
-    const clientModule = new ClientModule({
+    const clientModule = new Client({
       url: 'test',
       transformExchange: x => subscriptionExchange(createSubscription, x),
     });
@@ -990,7 +988,7 @@ describe('Client Component', () => {
     let result;
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         client={clientModule}
         query={[{ query }, { query }]}
         subscription={{ query: `subscription { todos { id } }` }}
@@ -1024,12 +1022,16 @@ describe('Client Component', () => {
 
   it('should unsubscribe before starting a new query', () => {
     (global as any).fetch.mockReturnValue(new Promise(() => {}));
-    const clientModule = new ClientModule({ url: 'test' });
+    const clientModule = new Client({ url: 'test' });
     const query = `{ todos { id } }`;
 
     // @ts-ignore
     const client = renderer.create(
-      <Client client={clientModule} query={{ query }} children={() => null} />
+      <UrqlClient
+        client={clientModule}
+        query={{ query }}
+        children={() => null}
+      />
     );
 
     expect(client.getInstance().querySub).not.toBeNull();
@@ -1043,14 +1045,14 @@ describe('Client Component', () => {
 
   it('should unsubscribe before starting a new subscription', () => {
     const createSubscription = (_, _observer) => ({ unsubscribe: () => {} });
-    const clientModule = new ClientModule({
+    const clientModule = new Client({
       url: 'test',
       transformExchange: x => subscriptionExchange(createSubscription, x),
     });
 
     // @ts-ignore
     const client = renderer.create(
-      <Client
+      <UrqlClient
         client={clientModule}
         subscription={{ query: `subscription { ideas { id } }` }}
         children={() => null}
diff --git a/src/tests/components/connect-hoc.test.tsx b/src/components/connect-hoc.test.tsx
similarity index 97%
rename from src/tests/components/connect-hoc.test.tsx
rename to src/components/connect-hoc.test.tsx
index cf0d8b00..4527a5b1 100644
--- a/src/tests/components/connect-hoc.test.tsx
+++ b/src/components/connect-hoc.test.tsx
@@ -1,7 +1,7 @@
 /* tslint:disable */
 
 import React, { Component } from 'react';
-import ConnectHOC from '../../components/connect-hoc';
+import ConnectHOC from './connect-hoc';
 import renderer from 'react-test-renderer';
 
 describe('Connect HOC', () => {
diff --git a/src/tests/components/connect.test.tsx b/src/components/connect.test.tsx
similarity index 89%
rename from src/tests/components/connect.test.tsx
rename to src/components/connect.test.tsx
index 0ab66f07..a4edf5a6 100644
--- a/src/tests/components/connect.test.tsx
+++ b/src/components/connect.test.tsx
@@ -1,7 +1,7 @@
 /* tslint:disable */
 
 import React from 'react';
-import Connect from '../../components/connect';
+import { Connect } from './connect';
 import renderer from 'react-test-renderer';
 
 describe('Client Component', () => {
diff --git a/src/components/context.test.tsx b/src/components/context.test.tsx
new file mode 100644
index 00000000..7398a225
--- /dev/null
+++ b/src/components/context.test.tsx
@@ -0,0 +1,10 @@
+/* tslint:disable */
+
+import * as exp from './context';
+
+describe('Context', () => {
+  it('should export a new react context', () => {
+    expect(exp.ContextConsumer).toBeTruthy();
+    expect(exp.ContextProvider).toBeTruthy();
+  });
+});
diff --git a/src/tests/components/provider.test.tsx b/src/components/provider.test.tsx
similarity index 89%
rename from src/tests/components/provider.test.tsx
rename to src/components/provider.test.tsx
index c8d25591..2148d254 100644
--- a/src/tests/components/provider.test.tsx
+++ b/src/components/provider.test.tsx
@@ -1,7 +1,7 @@
 /* tslint:disable */
 
 import React from 'react';
-import Provider from '../../components/provider';
+import { Provider } from './provider';
 import renderer from 'react-test-renderer';
 
 describe('Provider Component', () => {
diff --git a/src/tests/modules/__snapshots__/http-exchange.test.ts.snap b/src/exchanges/__snapshots__/http.test.ts.snap
similarity index 100%
rename from src/tests/modules/__snapshots__/http-exchange.test.ts.snap
rename to src/exchanges/__snapshots__/http.test.ts.snap
diff --git a/src/tests/modules/cache-exchange.test.ts b/src/exchanges/cache.test.ts
similarity index 79%
rename from src/tests/modules/cache-exchange.test.ts
rename to src/exchanges/cache.test.ts
index 0baf870e..ca3ced80 100755
--- a/src/tests/modules/cache-exchange.test.ts
+++ b/src/exchanges/cache.test.ts
@@ -1,7 +1,7 @@
 import Observable from 'zen-observable-ts';
-import { IClient, IExchange, IOperation } from '../../interfaces/index';
-import { cacheExchange } from '../../modules/cache-exchange';
-import { defaultCache } from '../../modules/default-cache';
+import { Exchange, Operation } from '../types';
+import { cacheExchange } from '.';
+import { Client, defaultCache } from '../lib';
 
 const result = {
   data: {
@@ -20,9 +20,9 @@ describe('cacheExchange', () => {
   });
 
   it('ignores unrelated operations', () => {
-    const client = ({} as any) as IClient;
+    const client = ({} as any) as Client;
     const res = Observable.of(undefined);
-    const forward: IExchange = () => res;
+    const forward: Exchange = () => res;
     const exchange = cacheExchange(client, forward);
 
     const operation = ({
@@ -30,7 +30,7 @@ describe('cacheExchange', () => {
       key: 'test',
       operationName: 'subscription',
       query: '{ test }',
-    } as any) as IOperation;
+    } as any) as Operation;
 
     expect(exchange(operation)).toBe(res);
   });
@@ -39,8 +39,8 @@ describe('cacheExchange', () => {
     const testkey = 'TESTKEY';
     const cache = defaultCache({ [testkey]: result });
     const updateCacheEntry = jest.fn().mockReturnValue(Promise.resolve());
-    const client = ({ cache, updateCacheEntry } as any) as IClient;
-    const forward: IExchange = () => Observable.of(undefined);
+    const client = ({ cache, updateCacheEntry } as any) as Client;
+    const forward: Exchange = () => Observable.of(undefined);
     const exchange = cacheExchange(client, forward);
 
     const operation = ({
@@ -48,7 +48,7 @@ describe('cacheExchange', () => {
       key: testkey,
       operationName: 'query',
       query: '{ test }',
-    } as any) as IOperation;
+    } as any) as Operation;
 
     exchange(operation).subscribe(res => {
       expect(res).toBe(result);
@@ -65,13 +65,13 @@ describe('cacheExchange', () => {
       key: testkey,
       operationName: 'query',
       query: '{ test }',
-    } as any) as IOperation;
+    } as any) as Operation;
 
     const newResult = { ...result, operation, test: true };
     const cache = defaultCache({ [testkey]: result });
     const updateCacheEntry = jest.fn().mockReturnValue(Promise.resolve());
-    const client = ({ cache, updateCacheEntry } as any) as IClient;
-    const forward: IExchange = () => Observable.of(newResult);
+    const client = ({ cache, updateCacheEntry } as any) as Client;
+    const forward: Exchange = () => Observable.of(newResult);
     const exchange = cacheExchange(client, forward);
 
     exchange(operation).subscribe(res => {
@@ -91,8 +91,8 @@ describe('cacheExchange', () => {
       cache,
       updateCacheEntry,
       deleteCacheKeys,
-    } as any) as IClient;
-    const forward: IExchange = operation =>
+    } as any) as Client;
+    const forward: Exchange = operation =>
       Observable.of({ ...result, operation });
     const exchange = cacheExchange(client, forward);
 
@@ -101,14 +101,14 @@ describe('cacheExchange', () => {
       key: testkey,
       operationName: 'query',
       query: '{ test }',
-    } as any) as IOperation;
+    } as any) as Operation;
 
     const operationB = ({
       context: {},
       key: 'anything',
       operationName: 'mutation',
       query: '{ test }',
-    } as any) as IOperation;
+    } as any) as Operation;
 
     exchange(operationA).subscribe(() => {
       expect(updateCacheEntry).toHaveBeenCalledWith(testkey, {
diff --git a/src/tests/modules/dedup-exchange.test.ts b/src/exchanges/dedup.test.ts
similarity index 90%
rename from src/tests/modules/dedup-exchange.test.ts
rename to src/exchanges/dedup.test.ts
index a9fd670e..2f38620d 100644
--- a/src/tests/modules/dedup-exchange.test.ts
+++ b/src/exchanges/dedup.test.ts
@@ -1,6 +1,6 @@
 import Observable from 'zen-observable-ts';
-import { IExchange } from '../../interfaces/exchange';
-import { dedupExchange } from '../../modules/dedup-exchange';
+import { Exchange } from '../types';
+import { dedupExchange } from './dedup';
 
 describe('dedupExchange', () => {
   beforeEach(() => {
@@ -17,7 +17,7 @@ describe('dedupExchange', () => {
         mockOp = operation;
         observer.next(operation);
         observer.complete();
-      }) as any) as IExchange;
+      }) as any) as Exchange;
 
     const testExchange = dedupExchange(mockExchange);
     const testOperation = { key: 'test' } as any;
@@ -32,7 +32,7 @@ describe('dedupExchange', () => {
   });
 
   it('returns the same intermediate observable when called with same operation', () => {
-    const mockExchange = jest.fn() as IExchange;
+    const mockExchange = jest.fn() as Exchange;
     const testExchange = dedupExchange(mockExchange);
 
     const obsA = testExchange({ key: 'a' } as any);
@@ -50,7 +50,7 @@ describe('dedupExchange', () => {
       new Observable(observer => {
         observer.next(null);
         observer.complete();
-      }) as any) as IExchange;
+      }) as any) as Exchange;
 
     const testExchange = dedupExchange(mockExchange);
     const obsA = testExchange({ key: 'a' } as any);
diff --git a/src/tests/modules/http-exchange.test.ts b/src/exchanges/http.test.ts
similarity index 97%
rename from src/tests/modules/http-exchange.test.ts
rename to src/exchanges/http.test.ts
index 2bcb4439..1b2de274 100755
--- a/src/tests/modules/http-exchange.test.ts
+++ b/src/exchanges/http.test.ts
@@ -1,6 +1,6 @@
-import { IOperation } from '../../interfaces/index';
-import { CombinedError } from '../../modules/error';
-import { httpExchange } from '../../modules/http-exchange';
+import { Operation } from '../types';
+import { CombinedError } from '../lib';
+import { httpExchange } from './http';
 
 describe('httpExchange', () => {
   beforeEach(() => {
@@ -276,7 +276,7 @@ describe('httpExchange', () => {
       httpExchange()(({
         context: {},
         operationName: 'subscription',
-      } as any) as IOperation);
+      } as any) as Operation);
     }).toThrowErrorMatchingSnapshot();
   });
 });
diff --git a/src/tests/modules/subscription-exchange.test.ts b/src/exchanges/subscription.test.ts
similarity index 90%
rename from src/tests/modules/subscription-exchange.test.ts
rename to src/exchanges/subscription.test.ts
index 3d9e7aeb..a2876566 100755
--- a/src/tests/modules/subscription-exchange.test.ts
+++ b/src/exchanges/subscription.test.ts
@@ -1,6 +1,6 @@
-import { IOperation } from '../../interfaces/index';
-import { subscriptionExchange } from '../../modules/subscription-exchange';
-import { CombinedError } from '../../modules/error';
+import { Operation } from '../types';
+import { subscriptionExchange } from './subscription';
+import { CombinedError } from '../lib';
 
 describe('subscriptionExchange', () => {
   beforeEach(() => {
@@ -16,7 +16,7 @@ describe('subscriptionExchange', () => {
 
     exchange(({
       operationName: 'query',
-    } as any) as IOperation);
+    } as any) as Operation);
 
     expect(createSubscription).not.toHaveBeenCalled();
     expect(forward).toHaveBeenCalled();
@@ -26,7 +26,7 @@ describe('subscriptionExchange', () => {
     const exOperation = ({
       key: 'test',
       operationName: 'subscription',
-    } as any) as IOperation;
+    } as any) as Operation;
 
     const next = jest.fn();
     const error = jest.fn();
@@ -85,7 +85,7 @@ describe('subscriptionExchange', () => {
     const exOperation = ({
       key: 'test',
       operationName: 'subscription',
-    } as any) as IOperation;
+    } as any) as Operation;
 
     const unsubscribe = jest.fn();
     const createSubscription = () => ({ unsubscribe });
diff --git a/src/setupTests.ts b/src/setupTests.ts
new file mode 100644
index 00000000..2face259
--- /dev/null
+++ b/src/setupTests.ts
@@ -0,0 +1,10 @@
+'use strict';
+const g = global as any;
+
+g._AbortController = g.AbortController;
+g.AbortController = undefined;
+g.fetch = jest.fn();
+
+process.on('unhandledRejection', error => {
+  throw error;
+});
diff --git a/src/tests/components/context.test.tsx b/src/tests/components/context.test.tsx
deleted file mode 100644
index ff185abc..00000000
--- a/src/tests/components/context.test.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-/* tslint:disable */
-
-import * as exp from '../../components/context';
-
-describe('Context', () => {
-  it('should export a new react context', () => {
-    expect(exp.Consumer).toBeTruthy();
-    expect(exp.Provider).toBeTruthy();
-  });
-});
diff --git a/src/tests/modules/client.test.ts b/src/tests/modules/client.test.ts
deleted file mode 100755
index 5500553e..00000000
--- a/src/tests/modules/client.test.ts
+++ /dev/null
@@ -1,489 +0,0 @@
-import Client from '../../modules/client';
-import { defaultCache } from '../../modules/default-cache';
-import { ClientEventType } from '../../interfaces/events';
-
-describe('Client', () => {
-  beforeEach(() => {
-    jest.resetAllMocks();
-    jest.clearAllMocks();
-    jest.restoreAllMocks();
-  });
-
-  it('should throw without options provided', () => {
-    expect(() => {
-      /* tslint:disable-next-line no-unused-expression */
-      new Client();
-    }).toThrowError('Please provide configuration object');
-  });
-
-  it('should throw without a url provided', () => {
-    expect(() => {
-      // @ts-ignore
-      new Client({}); /* tslint:disable-line no-unused-expression */
-    }).toThrowError('Please provide a URL for your GraphQL API');
-  });
-
-  it('should return a client instance', () => {
-    const client = new Client({
-      url: 'test',
-    });
-    expect(client.url).toMatch('test');
-  });
-
-  it('should apply transformExchange to the exchange', () => {
-    const client = new Client({
-      transformExchange: x => {
-        (x as any).test = true;
-        return x;
-      },
-      url: 'test',
-    });
-
-    expect((client.exchange as any).test).toBe(true);
-  });
-
-  it('should set fetchOptions', () => {
-    const client = new Client({
-      fetchOptions: {
-        test: 5,
-      },
-      url: 'test',
-    });
-    expect(client.fetchOptions).toMatchObject({ test: 5 });
-  });
-
-  it('should set fetchOptions to an object if not provided', () => {
-    const client = new Client({
-      url: 'test',
-    });
-    expect(client.fetchOptions).toMatchObject({});
-  });
-
-  it('should set intialCache if provided', () => {
-    const client = new Client({
-      url: 'test',
-      initialCache: {
-        test: 5,
-      },
-    });
-    expect(client.store).toMatchObject({ test: 5 });
-  });
-
-  it('should set cache if provided', () => {
-    const myCache = defaultCache({});
-
-    const client = new Client({
-      cache: myCache,
-      url: 'test',
-    });
-
-    expect(client.cache).toMatchObject(myCache);
-  });
-
-  describe('cache', () => {
-    it('should provide a valid cache by default', () => {
-      const store = { test: 5 };
-      const myCache = defaultCache(store);
-
-      const client = new Client({
-        cache: myCache,
-        url: 'test',
-      });
-
-      expect(client.cache.invalidate).toBeTruthy();
-      expect(client.cache.invalidateAll).toBeTruthy();
-      expect(client.cache.read).toBeTruthy();
-      expect(client.cache.update).toBeTruthy();
-      expect(client.cache.write).toBeTruthy();
-    });
-  });
-
-  describe('refreshAllFromCache', () => {
-    let client;
-
-    beforeAll(() => {
-      client = new Client({
-        url: 'test',
-      });
-    });
-
-    it('should call all registered subscribers with the last argument of true', () => {
-      const spy = jest.fn();
-      client.subscribe(spy);
-      client.refreshAllFromCache();
-
-      const event = { type: ClientEventType.RefreshAll };
-      expect(spy).toBeCalledWith(event.type, undefined);
-    });
-  });
-
-  describe('deleteCacheKeys', () => {
-    let client;
-
-    beforeAll(() => {
-      client = new Client({
-        url: 'test',
-      });
-    });
-
-    it('should invalidate key on cache and call listeners', done => {
-      const cacheSpy = jest.spyOn(client.cache, 'invalidate');
-      const listener = jest.fn();
-      client.subscribe(listener);
-      client.deleteCacheKeys(['test']);
-
-      setTimeout(() => {
-        expect(listener).toBeCalledWith(ClientEventType.CacheKeysInvalidated, [
-          'test',
-        ]);
-        expect(cacheSpy).toBeCalledWith('test');
-        done();
-      });
-    });
-  });
-
-  describe('updateCacheEntry', () => {
-    let client;
-
-    beforeAll(() => {
-      client = new Client({
-        url: 'test',
-      });
-    });
-
-    it('should update key on cache and call listeners', done => {
-      const cacheSpy = jest.spyOn(client.cache, 'write');
-      const listener = jest.fn();
-      client.subscribe(listener);
-      const val = { test: true };
-      client.updateCacheEntry('test', val);
-
-      setTimeout(() => {
-        expect(listener).toBeCalledWith(ClientEventType.CacheKeysInvalidated, [
-          'test',
-        ]);
-        expect(cacheSpy).toBeCalledWith('test', val);
-        done();
-      });
-    });
-  });
-
-  describe('invalidateQuery', () => {
-    let client;
-
-    beforeAll(() => {
-      client = new Client({
-        url: 'test',
-      });
-    });
-
-    it('should invalidate key on cache and call listeners', done => {
-      const cacheSpy = jest.spyOn(client.cache, 'invalidate');
-      const listener = jest.fn();
-      client.subscribe(listener);
-      client.invalidateQuery({ query: '{ test }', variables: {} });
-
-      setTimeout(() => {
-        expect(listener).toBeCalledWith(ClientEventType.CacheKeysInvalidated, [
-          expect.any(String),
-        ]);
-        expect(cacheSpy).toBeCalledWith(expect.any(String));
-        done();
-      });
-    });
-  });
-
-  describe('subscribe', () => {
-    let client;
-
-    beforeEach(() => {
-      client = new Client({
-        url: 'test',
-      });
-    });
-
-    it('should return an unsubscribe callback', () => {
-      const callback = () => {};
-      const unsubscribe = client.subscribe(callback);
-      expect(unsubscribe).not.toBeNull();
-    });
-
-    it('should add function arguments to the internal subscriptions object', () => {
-      const callback = () => {};
-      expect(Object.keys(client.subscriptions).length).toBe(0);
-      client.subscribe(callback);
-      expect(Object.keys(client.subscriptions).length).toBe(1);
-    });
-  });
-
-  describe('unsubscribe', () => {
-    let client;
-
-    beforeEach(() => {
-      client = new Client({
-        url: 'test',
-      });
-    });
-
-    it('should remove from subscriptions by id', () => {
-      const callback = () => {};
-      const unsubscribe = client.subscribe(callback);
-      expect(Object.keys(client.subscriptions).length).toBe(1);
-      unsubscribe();
-      expect(Object.keys(client.subscriptions).length).toBe(0);
-    });
-  });
-
-  describe('executeQuery', () => {
-    let client;
-
-    it('should return data if there is data', done => {
-      client = new Client({
-        url: 'http://localhost:3000/graphql',
-      });
-      (global as any).fetch.mockReturnValue(
-        Promise.resolve({
-          status: 200,
-          json: () => ({ data: [{ id: 5 }] }),
-        })
-      );
-
-      client
-        .executeQuery({
-          query: `{
-          todos {
-            id
-            name
-          }
-        }`,
-        })
-        .then(result => {
-          expect(result.data).toEqual([{ id: 5 }]);
-          done();
-        });
-    });
-
-    it('should include fetchOptions', done => {
-      client = new Client({
-        url: 'http://localhost:3000/graphql',
-        fetchOptions: {
-          test: 5,
-        },
-      });
-
-      (global as any).fetch.mockReturnValue(
-        Promise.resolve({
-          status: 200,
-          json: () => ({ data: [{ id: 5 }] }),
-        })
-      );
-
-      client
-        .executeQuery({
-          query: '{ test }',
-        })
-        .then(() => {
-          expect((global as any).fetch).toHaveBeenCalledWith(
-            'http://localhost:3000/graphql',
-            {
-              body: expect.any(String),
-              headers: { 'Content-Type': 'application/json' },
-              method: 'POST',
-              test: 5,
-            }
-          );
-
-          done();
-        });
-    });
-
-    it('should include functional fetchOptions', done => {
-      client = new Client({
-        fetchOptions: () => ({ test: 5 }),
-        url: 'http://localhost:3000/graphql',
-      });
-
-      (global as any).fetch.mockReturnValue(
-        Promise.resolve({
-          json: () => ({ data: [{ id: 5 }] }),
-          status: 200,
-        })
-      );
-
-      client.executeQuery({ query: '{ test }' }).then(() => {
-        expect((global as any).fetch).toHaveBeenCalledWith(
-          'http://localhost:3000/graphql',
-          {
-            body: expect.any(String),
-            headers: { 'Content-Type': 'application/json' },
-            method: 'POST',
-            test: 5,
-          }
-        );
-
-        done();
-      });
-    });
-
-    it('should return "No Data" if data is not present', done => {
-      client = new Client({
-        url: 'http://localhost:3000/graphql',
-      });
-
-      (global as any).fetch.mockReturnValue(
-        Promise.resolve({
-          status: 200,
-          json: () => ({ test: 5 }),
-        })
-      );
-
-      client
-        .executeQuery({
-          query: `{
-          todos {
-            id
-            name
-          }
-        }`,
-        })
-        .catch(e => {
-          expect(e.message).toBe('no data or error');
-          done();
-        });
-    });
-
-    it('should return an error if fetch throws', done => {
-      client = new Client({
-        url: 'http://localhost:3000/graphql',
-      });
-
-      (global as any).fetch.mockReturnValue(
-        Promise.reject(new Error('Nooooo'))
-      );
-
-      client
-        .executeQuery({
-          query: `{
-          todos {
-            id
-            name
-          }
-        }`,
-        })
-        .catch(e => {
-          expect(e.networkError).toMatchObject(new Error('Nooooo'));
-          done();
-        });
-    });
-  });
-
-  describe('executeMutation', () => {
-    let client;
-    it('should return specified data if present', done => {
-      client = new Client({
-        url: 'http://localhost:3000/graphql',
-      });
-
-      (global as any).fetch.mockReturnValue(
-        Promise.resolve({
-          status: 200,
-          json: () => ({ data: { test: 5 } }),
-        })
-      );
-
-      client
-        .executeMutation({
-          query: `{
-          todos {
-            id
-            name
-          }
-        }`,
-        })
-        .then(data => {
-          expect(data).toMatchObject({ test: 5 });
-          done();
-        });
-    });
-
-    it('should return "No Data" if data is not present', done => {
-      client = new Client({
-        url: 'http://localhost:3000/graphql',
-      });
-
-      (global as any).fetch.mockReturnValue(
-        Promise.resolve({
-          status: 200,
-          json: () => ({ test: 5 }),
-        })
-      );
-
-      client
-        .executeMutation({
-          query: `{
-          todos {
-            id
-            name
-          }
-        }`,
-        })
-        .catch(e => {
-          expect(e.message).toBe('no data or error');
-          done();
-        });
-    });
-
-    it('should return an error if fetch throws', done => {
-      client = new Client({
-        url: 'http://localhost:3000/graphql',
-      });
-
-      (global as any).fetch.mockReturnValue(Promise.reject(new Error('Noooo')));
-
-      client
-        .executeMutation({
-          query: `{
-          todos {
-            id
-            name
-          }
-        }`,
-        })
-        .catch(e => {
-          expect(e.networkError).toMatchObject(new Error('Noooo'));
-          done();
-        });
-    });
-
-    it('should return an error with attached response if fetch throws an HTTP error', () => {
-      client = new Client({
-        url: 'http://localhost:3000/graphql',
-      });
-
-      (global as any).fetch.mockReturnValue(
-        Promise.resolve({
-          status: 401,
-          statusText: "I'm afraid I can't let you do that, Dave",
-        })
-      );
-
-      return client
-        .executeMutation({
-          query: `{
-          todos {
-            id
-            name
-          }
-        }`,
-        })
-        .catch(e => {
-          expect(e).toHaveProperty('name', 'CombinedError');
-          expect(e).toHaveProperty(
-            'response.statusText',
-            "I'm afraid I can't let you do that, Dave"
-          );
-          expect(e).toHaveProperty('response.status', 401);
-        });
-    });
-  });
-});
diff --git a/src/tests/modules/default-cache.test.ts b/src/tests/modules/default-cache.test.ts
deleted file mode 100644
index 81b49b72..00000000
--- a/src/tests/modules/default-cache.test.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { defaultCache } from '../../modules/default-cache';
-
-describe('defaultCache', () => {
-  beforeEach(() => {
-    jest.resetAllMocks();
-    jest.clearAllMocks();
-    jest.restoreAllMocks();
-  });
-
-  it('should provide a valid cache by default', done => {
-    const store = { test: 5 };
-    const cache = defaultCache(store);
-
-    Promise.all([
-      cache.invalidateAll(),
-      cache.read('test'),
-      cache.write('test', 5),
-      cache.read('test'),
-      cache.update((acc, key) => {
-        if (key === 'test') {
-          acc[key] = 6;
-        }
-      }),
-      cache.update(null),
-      cache.read('test'),
-      cache.invalidate('test'),
-      cache.read('test'),
-    ]).then(d => {
-      expect(d).toEqual([
-        undefined,
-        null,
-        'test',
-        5,
-        undefined,
-        undefined,
-        6,
-        'test',
-        null,
-      ]);
-
-      done();
-    });
-  });
-});
diff --git a/src/tests/modules/error.test.ts b/src/tests/modules/error.test.ts
deleted file mode 100644
index 3678448e..00000000
--- a/src/tests/modules/error.test.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { CombinedError } from '../../modules/error';
-
-describe('CombinedError', () => {
-  it('can be instantiated with graphQLErrors', () => {
-    const err = new CombinedError({
-      graphQLErrors: [],
-    });
-
-    expect(err.name).toBe('CombinedError');
-  });
-
-  it('accepts graphQLError messages and generates a single message from them', () => {
-    const graphQLErrors = ['Error Message A', 'Error Message B'];
-
-    const err = new CombinedError({ graphQLErrors });
-
-    expect(err.message).toBe(
-      `
-[GraphQL] Error Message A
-[GraphQL] Error Message B
-    `.trim()
-    );
-
-    expect(err.graphQLErrors).toEqual(graphQLErrors.map(x => new Error(x)));
-  });
-
-  it('accepts a network error and generates a message from it', () => {
-    const networkError = new Error('Network Shenanigans');
-    const err = new CombinedError({ networkError });
-
-    expect(err.message).toBe(`[Network] ${networkError.message}`);
-  });
-
-  it('accepts actual errors for graphQLError', () => {
-    const graphQLErrors = [
-      new Error('Error Message A'),
-      new Error('Error Message B'),
-    ];
-
-    const err = new CombinedError({ graphQLErrors });
-
-    expect(err.message).toBe(
-      `
-[GraphQL] Error Message A
-[GraphQL] Error Message B
-    `.trim()
-    );
-
-    expect(err.graphQLErrors).toEqual(graphQLErrors);
-  });
-
-  it('passes graphQLErrors through as a last resort', () => {
-    const graphQLErrors = [{ x: 'y' }] as any;
-    const err = new CombinedError({ graphQLErrors });
-
-    expect(err.graphQLErrors).toEqual(graphQLErrors);
-  });
-
-  it('accepts a response that is attached to the resulting error', () => {
-    const response = {};
-    const err = new CombinedError({
-      graphQLErrors: [],
-      response,
-    });
-
-    expect(err.response).toBe(response);
-  });
-});
diff --git a/src/tests/modules/hash.test.ts b/src/tests/modules/hash.test.ts
deleted file mode 100644
index 18e3c89d..00000000
--- a/src/tests/modules/hash.test.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { hashString } from '../../modules/hash';
-
-describe('hash', () => {
-  it('should returned a murmur hashed string from a query string', () => {
-    let hash = hashString(`{ todos { id } }`);
-    expect(hash).toBe('1rvkz44');
-  });
-});
diff --git a/src/tests/modules/query.test.ts b/src/tests/modules/query.test.ts
deleted file mode 100644
index 7558032d..00000000
--- a/src/tests/modules/query.test.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { query } from '../../modules/query';
-
-describe('query / mutation / subscription', () => {
-  it('should return a valid query object', () => {
-    const val = query(`{ todos { id } }`);
-    expect(val).toMatchObject({ query: `{ todos { id } }`, variables: {} });
-  });
-
-  it('should return a valid query object with variables', () => {
-    const val = query(`{ todos { id } }`, { test: 5 });
-    expect(val).toMatchObject({
-      query: `{ todos { id } }`,
-      variables: { test: 5 },
-    });
-  });
-});
diff --git a/src/tests/modules/typenames.test.ts b/src/tests/modules/typenames.test.ts
deleted file mode 100755
index b59fc12a..00000000
--- a/src/tests/modules/typenames.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import {
-  formatTypeNames,
-  gankTypeNamesFromResponse,
-} from '../../modules/typenames';
-
-describe('formatTypeNames', () => {
-  it('should add typenames to a query string', () => {
-    expect(formatTypeNames(`{ todos { id } }`)).toBe(`{
-  todos {
-    id
-    __typename
-  }
-}
-`);
-  });
-});
-
-describe('gankTypeNamesFromResponse', () => {
-  it('should return all typenames included in a response as an array', () => {
-    let typeNames = gankTypeNamesFromResponse({
-      todos: [
-        {
-          id: 1,
-
-          __typename: 'Todo',
-        },
-      ],
-    });
-    expect(typeNames).toMatchObject(['Todo']);
-  });
-});
diff --git a/src/tests/setup.js b/src/tests/setup.js
deleted file mode 100644
index 1afb6c76..00000000
--- a/src/tests/setup.js
+++ /dev/null
@@ -1,9 +0,0 @@
-'use strict';
-
-global._AbortController = global.AbortController;
-global.AbortController = undefined;
-global.fetch = jest.fn();
-
-process.on('unhandledRejection', error => {
-  throw error;
-});
diff --git a/src/tests/utils/zip-observables.test.ts b/src/utils/zip-observables.test.ts
similarity index 96%
rename from src/tests/utils/zip-observables.test.ts
rename to src/utils/zip-observables.test.ts
index 61e08a2d..648cafaf 100644
--- a/src/tests/utils/zip-observables.test.ts
+++ b/src/utils/zip-observables.test.ts
@@ -1,5 +1,5 @@
 import Observable from 'zen-observable-ts';
-import { zipObservables } from '../../utils/zip-observables';
+import { zipObservables } from './zip-observables';
 
 describe('zipObservables', () => {
   beforeEach(() => {