Browse Source

Add tests for mocks and jupyter server

Steven Silvester 5 years ago
parent
commit
8fb2f4aa91

+ 1 - 0
testutils/babel.config.js

@@ -0,0 +1 @@
+module.exports = require('./lib/babel.config');

+ 2 - 0
testutils/jest.config.js

@@ -0,0 +1,2 @@
+const func = require('./lib/jest-config');
+module.exports = func(__dirname);

+ 6 - 1
testutils/package.json

@@ -26,9 +26,14 @@
   },
   "scripts": {
     "build": "tsc -b",
+    "build:test": "tsc -b && tsc --build tsconfig.test.json",
     "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
     "prepublishOnly": "npm run build",
-    "watch": "tsc --watch"
+    "test": "jest",
+    "test:cov": "jest --collect-coverage",
+    "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+    "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
+    "watch": "tsc -b --watch"
   },
   "dependencies": {
     "@jupyterlab/apputils": "^2.1.0",

+ 15 - 12
testutils/src/mock.ts

@@ -28,6 +28,9 @@ import { Signal } from '@lumino/signaling';
 
 import { PathExt } from '@jupyterlab/coreutils';
 
+// The default kernel name
+export const DEFAULT_NAME = 'python3';
+
 export const KERNELSPECS: KernelSpec.ISpecModel[] = [
   {
     argv: [
@@ -40,7 +43,7 @@ export const KERNELSPECS: KernelSpec.ISpecModel[] = [
     display_name: 'Python 3',
     language: 'python',
     metadata: {},
-    name: 'python3',
+    name: DEFAULT_NAME,
     resources: {}
   },
   {
@@ -61,7 +64,7 @@ export const KERNELSPECS: KernelSpec.ISpecModel[] = [
 
 export const KERNEL_MODELS: Kernel.IModel[] = [
   {
-    name: 'python3',
+    name: DEFAULT_NAME,
     id: UUID.uuid4()
   },
   {
@@ -69,7 +72,7 @@ export const KERNEL_MODELS: Kernel.IModel[] = [
     id: UUID.uuid4()
   },
   {
-    name: 'python3',
+    name: DEFAULT_NAME,
     id: UUID.uuid4()
   }
 ];
@@ -156,7 +159,7 @@ export const KernelMock = jest.fn<
     (model! as any).id = 'foo';
   }
   if (!model.name) {
-    (model! as any).name = KERNEL_MODELS[0].name;
+    (model! as any).name = DEFAULT_NAME;
   }
   options = {
     clientId: UUID.uuid4(),
@@ -171,9 +174,7 @@ export const KernelMock = jest.fn<
     ...options,
     ...model,
     status: 'idle',
-    spec: () => {
-      return Promise.resolve(spec);
-    },
+    spec: Promise.resolve(spec),
     dispose: jest.fn(),
     clone: jest.fn(() => {
       const newKernel = Private.cloneKernel(options);
@@ -186,7 +187,7 @@ export const KernelMock = jest.fn<
       });
       return newKernel;
     }),
-    info: jest.fn(() => Promise.resolve(void 0)),
+    info: Promise.resolve(void 0),
     shutdown: jest.fn(() => Promise.resolve(void 0)),
     requestHistory: jest.fn(() => {
       const historyReply = KernelMessage.createMessage({
@@ -262,7 +263,7 @@ export const SessionConnectionMock = jest.fn<
     Kernel.IKernelConnection | null
   ]
 >((options, kernel) => {
-  const name = kernel?.name || options.model?.name || KERNEL_MODELS[0].name;
+  const name = kernel?.name || options.model?.kernel?.name || DEFAULT_NAME;
   kernel = kernel || new KernelMock({ model: { name } });
   const model = {
     path: 'foo',
@@ -282,7 +283,6 @@ export const SessionConnectionMock = jest.fn<
     changeKernel: jest.fn(partialModel => {
       return Private.changeKernel(kernel!, partialModel!);
     }),
-    selectKernel: jest.fn(),
     shutdown: jest.fn(() => Promise.resolve(void 0)),
     setPath: jest.fn(path => {
       (thisObject as any).path = path;
@@ -441,7 +441,6 @@ export const ContentsManagerMock = jest.fn<Contents.IManager, []>(() => {
 
   const thisObject: Contents.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
-    ready: Promise.resolve(void 0),
     newUntitled: jest.fn(options => {
       const model = Private.createFile(options || {});
       files.set(model.path, model);
@@ -603,6 +602,7 @@ export const SessionManagerMock = jest.fn<Session.IManager, []>(() => {
   const thisObject: Session.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
     ready: Promise.resolve(void 0),
+    isReady: true,
     startNew: jest.fn(options => {
       const session = new SessionConnectionMock({ model: options }, null);
       sessions.push(session.model);
@@ -638,6 +638,8 @@ export const KernelSpecManagerMock = jest.fn<KernelSpec.IManager, []>(() => {
   const thisObject: KernelSpec.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
     specs: { default: KERNELSPECS[0].name, kernelspecs: KERNELSPECS },
+    isReady: true,
+    ready: Promise.resolve(void 0),
     refreshSpecs: jest.fn(() => Promise.resolve(void 0))
   };
   return thisObject;
@@ -650,6 +652,7 @@ export const ServiceManagerMock = jest.fn<ServiceManager.IManager, []>(() => {
   const thisObject: ServiceManager.IManager = {
     ...jest.requireActual('@jupyterlab/services'),
     ready: Promise.resolve(void 0),
+    isReady: true,
     contents: new ContentsManagerMock(),
     sessions: new SessionManagerMock(),
     kernelspecs: new KernelSpecManagerMock(),
@@ -765,7 +768,7 @@ namespace Private {
   export function cloneKernel(
     options: RecursivePartial<Kernel.IKernelConnection.IOptions>
   ): Kernel.IKernelConnection {
-    return new KernelMock(options);
+    return new KernelMock({ ...options, clientId: UUID.uuid4() });
   }
 
   // Get the kernel spec for kernel name

+ 7 - 6
testutils/src/start_jupyter_server.ts

@@ -35,15 +35,15 @@ export class JupyterServer {
   /**
    * Start the server.
    *
-   * @returns A promise that resolves when the server has started
+   * @returns A promise that resolves with the url of the server
    *
    * @throws Error if another server is still running.
    */
-  async start(): Promise<void> {
+  async start(): Promise<string> {
     if (Private.child !== null) {
       throw Error('Previous server was not disposed');
     }
-    const startDelegate = new PromiseDelegate<void>();
+    const startDelegate = new PromiseDelegate<string>();
 
     const env = {
       JUPYTER_CONFIG_DIR: Private.handleConfig(),
@@ -81,7 +81,8 @@ export class JupyterServer {
       handleOutput(String(data));
     });
 
-    await startDelegate.promise;
+    const url = await startDelegate.promise;
+    return url;
   }
 
   /**
@@ -273,13 +274,13 @@ namespace Private {
    */
   export async function connect(
     baseUrl: string,
-    startDelegate: PromiseDelegate<void>
+    startDelegate: PromiseDelegate<string>
   ): Promise<void> {
     // eslint-disable-next-line
     while (true) {
       try {
         await fetch(URLExt.join(baseUrl, 'api'));
-        startDelegate.resolve(void 0);
+        startDelegate.resolve(baseUrl);
         return;
       } catch (e) {
         // spin until we can connect to the server.

+ 648 - 0
testutils/test/mock.spec.ts

@@ -0,0 +1,648 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import 'jest';
+
+import * as Mock from '../src/mock';
+import { KernelMessage } from '@jupyterlab/services';
+import { toArray } from '@lumino/algorithm';
+
+describe('mock', () => {
+  describe('createSimpleSessionContext()', () => {
+    it('should create a session context', () => {
+      const context = Mock.createSimpleSessionContext();
+      expect(context.session!.kernel!.name).toEqual(Mock.DEFAULT_NAME);
+    });
+
+    it('should accept a session model', () => {
+      const context = Mock.createSimpleSessionContext({
+        name: 'hi',
+        path: 'foo',
+        type: 'bar',
+        kernel: { name: 'fizz' }
+      });
+      expect(context.name).toEqual('hi');
+      expect(context.path).toEqual('foo');
+      expect(context.type).toEqual('bar');
+      expect(context.session!.kernel!.name).toEqual('fizz');
+    });
+  });
+
+  describe('updateKernelStatus()', () => {
+    it('should update the kernel status', () => {
+      const context = Mock.createSimpleSessionContext();
+      let called = false;
+      context.statusChanged.connect((_, status) => {
+        if (status === 'dead') {
+          called = true;
+        }
+      });
+      Mock.updateKernelStatus(context, 'dead');
+      expect(context.session!.kernel!.status).toEqual('dead');
+      expect(called).toEqual(true);
+    });
+  });
+
+  describe('emitIopubMessage', () => {
+    it('should emit an iopub message', () => {
+      const context = Mock.createSimpleSessionContext();
+      const source = KernelMessage.createMessage({
+        channel: 'iopub',
+        msgType: 'execute_input',
+        session: 'foo',
+        username: 'bar',
+        msgId: 'fizz',
+        content: {
+          code: 'hello, world!',
+          execution_count: 0
+        }
+      });
+      let called = false;
+      context.iopubMessage.connect((_, msg) => {
+        expect(msg).toBe(source);
+        called = true;
+      });
+      Mock.emitIopubMessage(context, source);
+      expect(called).toBe(true);
+    });
+  });
+
+  describe('cloneKernel()', () => {
+    it('should clone a kernel', () => {
+      const kernel0 = new Mock.KernelMock({});
+      const kernel1 = Mock.cloneKernel(kernel0);
+      expect(kernel0.id).toBe(kernel1.id);
+      expect(kernel0.clientId).not.toBe(kernel1.clientId);
+    });
+  });
+
+  describe('KernelMock', () => {
+    describe('.constructor()', () => {
+      it('should create a mock kernel', () => {
+        const kernel = new Mock.KernelMock({});
+        expect(kernel.name).toBe(Mock.DEFAULT_NAME);
+      });
+
+      it('should take options', () => {
+        const kernel = new Mock.KernelMock({ model: { name: 'foo' } });
+        expect(kernel.name).toBe('foo');
+      });
+    });
+
+    describe('.spec()', () => {
+      it('should resolve with a kernel spec', async () => {
+        const kernel = new Mock.KernelMock({});
+        const spec = await kernel.spec;
+        expect(spec!.name).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const kernel = new Mock.KernelMock({});
+        kernel.dispose();
+      });
+    });
+
+    describe('.clone()', () => {
+      it('should clone the kernel', () => {
+        const kernel0 = new Mock.KernelMock({});
+        const kernel1 = kernel0.clone();
+        expect(kernel0.id).toBe(kernel1.id);
+        expect(kernel0.clientId).not.toBe(kernel1.clientId);
+      });
+    });
+
+    describe('.info', () => {
+      it('should resolve with undefined', async () => {
+        const kernel = new Mock.KernelMock({});
+        const info = await kernel.info;
+        expect(info).toBeUndefined();
+      });
+    });
+
+    describe('.shutdown()', () => {
+      it('should be a no-op', async () => {
+        const kernel = new Mock.KernelMock({});
+        await kernel.shutdown();
+      });
+    });
+
+    describe('.requestHistory()', () => {
+      it('should get the history info', async () => {
+        const kernel = new Mock.KernelMock({});
+        const reply = await kernel.requestHistory({} as any);
+        expect(reply.content.status).toBe('ok');
+      });
+    });
+
+    describe('.restart()', () => {
+      it('should be a no-op', async () => {
+        const kernel = new Mock.KernelMock({});
+        await kernel.restart();
+      });
+    });
+
+    describe('.requestExecute()', () => {
+      it('should request execution', async () => {
+        const kernel = new Mock.KernelMock({});
+        let called = false;
+        kernel.iopubMessage.connect((_, msg) => {
+          if (msg.header.msg_type === 'execute_input') {
+            called = true;
+          }
+        });
+        const future = kernel.requestExecute({ code: 'foo ' });
+        await future.done;
+        expect(called).toBe(true);
+      });
+    });
+  });
+
+  describe('SessionConnectionMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new SessionConnectionMock', () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        expect(session.kernel!.name).toBe(Mock.DEFAULT_NAME);
+      });
+
+      it('should take options', () => {
+        const kernel = new Mock.KernelMock({});
+        const session = new Mock.SessionConnectionMock(
+          { model: { name: 'foo' } },
+          kernel
+        );
+        expect(session.kernel).toBe(kernel);
+        expect(session.name).toBe('foo');
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        session.dispose();
+      });
+    });
+
+    describe('.changeKernel()', () => {
+      it('should change the kernel', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        const oldId = session.kernel!.id;
+        const kernel = await session.changeKernel({ name: Mock.DEFAULT_NAME });
+        expect(kernel!.id).not.toBe(oldId);
+      });
+    });
+
+    describe('.shutdown()', () => {
+      it('should be a no-op', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        await session.shutdown();
+      });
+    });
+
+    describe('.setPath()', () => {
+      it('should set the path', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        let called = false;
+        session.propertyChanged.connect((_, args) => {
+          if (args === 'path') {
+            called = true;
+          }
+        });
+        await session.setPath('foo');
+        expect(session.path).toBe('foo');
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.setType()', () => {
+      it('should set the type', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        let called = false;
+        session.propertyChanged.connect((_, args) => {
+          if (args === 'type') {
+            called = true;
+          }
+        });
+        await session.setType('foo');
+        expect(session.type).toBe('foo');
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.setName()', () => {
+      it('should set the name', async () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        let called = false;
+        session.propertyChanged.connect((_, args) => {
+          if (args === 'name') {
+            called = true;
+          }
+        });
+        await session.setName('foo');
+        expect(session.name).toBe('foo');
+        expect(called).toBe(true);
+      });
+    });
+  });
+
+  describe('SessionContextMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new context', () => {
+        const context = new Mock.SessionContextMock({}, null);
+        expect(context.session!.kernel!.name).toBe(Mock.DEFAULT_NAME);
+      });
+
+      it('should accept options', () => {
+        const session = new Mock.SessionConnectionMock({}, null);
+        const context = new Mock.SessionContextMock({ path: 'foo' }, session);
+        expect(context.session).toBe(session);
+        expect(context.path).toBe('foo');
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const context = new Mock.SessionContextMock({}, null);
+        context.dispose();
+      });
+    });
+
+    describe('.initialize()', () => {
+      it('should be a no-op', async () => {
+        const context = new Mock.SessionContextMock({}, null);
+        await context.initialize();
+      });
+    });
+
+    describe('.ready', () => {
+      it('should be a no-op', async () => {
+        const context = new Mock.SessionContextMock({}, null);
+        await context.ready;
+      });
+    });
+
+    describe('.changeKernel()', () => {
+      it('should change the kernel', async () => {
+        const context = new Mock.SessionContextMock({}, null);
+        const oldId = context.session!.kernel!.id;
+        const kernel = await context.changeKernel({ name: Mock.DEFAULT_NAME });
+        expect(kernel!.id).not.toBe(oldId);
+      });
+    });
+
+    describe('.shutdown()', () => {
+      it('should be a no-op', async () => {
+        const context = new Mock.SessionContextMock({}, null);
+        await context.shutdown();
+      });
+    });
+  });
+
+  describe('ContentsManagerMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new mock', () => {
+        const manager = new Mock.ContentsManagerMock();
+        expect(manager.localPath('foo')).toBe('foo');
+      });
+    });
+
+    describe('.newUntitled', () => {
+      it('should create a new text file', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        let called = false;
+        manager.fileChanged.connect((_, args) => {
+          if (args.type === 'new') {
+            called = true;
+          }
+        });
+        const contents = await manager.newUntitled();
+        expect(contents.type).toBe('file');
+        expect(called).toBe(true);
+      });
+
+      it('should create a new notebook', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const contents = await manager.newUntitled({ type: 'notebook' });
+        expect(contents.type).toBe('notebook');
+      });
+    });
+
+    describe('.createCheckpoint()', () => {
+      it('should create a checkpoint', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        const checkpoint = await manager.createCheckpoint(content.path);
+        expect(checkpoint.id).toBeTruthy();
+      });
+    });
+
+    describe('.listCheckpoints()', () => {
+      it('should list the checkpoints', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        const checkpoint = await manager.createCheckpoint(content.path);
+        const checkpoints = await manager.listCheckpoints(content.path);
+        expect(checkpoints[0].id).toBe(checkpoint.id);
+      });
+    });
+
+    describe('.deleteCheckpoint', () => {
+      it('should delete a checkpoints', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        const checkpoint = await manager.createCheckpoint(content.path);
+        await manager.deleteCheckpoint(content.path, checkpoint.id);
+        const checkpoints = await manager.listCheckpoints(content.path);
+        expect(checkpoints.length).toBe(0);
+      });
+    });
+
+    describe('.restoreCheckpoint()', () => {
+      it('should restore the contents', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        await manager.save(content.path, { content: 'foo' });
+        const checkpoint = await manager.createCheckpoint(content.path);
+        await manager.save(content.path, { content: 'bar' });
+        await manager.restoreCheckpoint(content.path, checkpoint.id);
+        const newContent = await manager.get(content.path);
+        expect(newContent.content).toBe('foo');
+      });
+    });
+
+    describe('.getModelDBFactory()', () => {
+      it('should return null', () => {
+        const manager = new Mock.ContentsManagerMock();
+        expect(manager.getModelDBFactory('foo')).toBe(null);
+      });
+    });
+
+    describe('.normalize()', () => {
+      it('should normalize a path', () => {
+        const manager = new Mock.ContentsManagerMock();
+        expect(manager.normalize('foo/bar/../baz')).toBe('foo/baz');
+      });
+    });
+
+    describe('.localPath', () => {
+      it('should get the local path of a file', () => {
+        const manager = new Mock.ContentsManagerMock();
+        const defaultDrive = manager.driveName('foo');
+        expect(manager.localPath(`${defaultDrive}foo/bar`)).toBe('foo/bar');
+      });
+    });
+
+    describe('.get()', () => {
+      it('should get the file contents', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        const content = await manager.newUntitled();
+        await manager.save(content.path, { content: 'foo' });
+        const newContent = await manager.get(content.path);
+        expect(newContent.content).toBe('foo');
+      });
+    });
+
+    describe('.driveName()', () => {
+      it('should get the drive name of the path', () => {
+        const manager = new Mock.ContentsManagerMock();
+        const defaultDrive = manager.driveName('foo');
+        expect(manager.driveName(`${defaultDrive}/bar`)).toBe(defaultDrive);
+      });
+    });
+
+    describe('.rename()', () => {
+      it('should rename the file', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        let called = false;
+        manager.fileChanged.connect((_, args) => {
+          if (args.type === 'rename') {
+            expect(args.newValue!.path).toBe('foo');
+            called = true;
+          }
+        });
+        const contents = await manager.newUntitled();
+        await manager.rename(contents.path, 'foo');
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.delete()', () => {
+      it('should delete the file', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        let called = false;
+        manager.fileChanged.connect((_, args) => {
+          if (args.type === 'delete') {
+            expect(args.newValue).toBe(null);
+            called = true;
+          }
+        });
+        const contents = await manager.newUntitled();
+        await manager.delete(contents.path);
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.save()', () => {
+      it('should save the file', async () => {
+        const manager = new Mock.ContentsManagerMock();
+        let called = false;
+        manager.fileChanged.connect((_, args) => {
+          if (args.type === 'save') {
+            expect(args.newValue!.content).toBe('bar');
+            called = true;
+          }
+        });
+        const contents = await manager.newUntitled();
+        await manager.save(contents.path, { content: 'bar' });
+        expect(called).toBe(true);
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const manager = new Mock.ContentsManagerMock();
+        manager.dispose();
+      });
+    });
+  });
+
+  describe('SessionManagerMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new session manager', () => {
+        const manager = new Mock.SessionManagerMock();
+        expect(manager.isReady).toBe(true);
+      });
+    });
+
+    describe('.startNew()', () => {
+      it('should start a new session', async () => {
+        const manager = new Mock.SessionManagerMock();
+        const session = await manager.startNew({
+          path: 'foo',
+          name: 'foo',
+          type: 'bar',
+          kernel: { name: Mock.DEFAULT_NAME }
+        });
+        expect(session.kernel!.name).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.connectTo()', () => {
+      it('should connect to a session', async () => {
+        const manager = new Mock.SessionManagerMock();
+        const session = await manager.connectTo({
+          model: {
+            id: 'fizz',
+            path: 'foo',
+            type: 'bar',
+            name: 'baz',
+            kernel: { name: Mock.DEFAULT_NAME, id: 'fuzz' }
+          }
+        });
+        expect(session.kernel!.name).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.stopIfNeeded()', () => {
+      it('should remove a running kernel', async () => {
+        const manager = new Mock.SessionManagerMock();
+        const session = await manager.startNew({
+          path: 'foo',
+          name: 'foo',
+          type: 'bar',
+          kernel: { name: Mock.DEFAULT_NAME }
+        });
+        expect(toArray(manager.running()).length).toBe(1);
+        await manager.stopIfNeeded(session.path);
+        expect(toArray(manager.running()).length).toBe(0);
+      });
+    });
+
+    describe('.refreshRunning()', () => {
+      it('should be a no-op', async () => {
+        const manager = new Mock.SessionManagerMock();
+        await manager.refreshRunning();
+      });
+    });
+
+    describe('.running()', () => {
+      it('should be an iterable of running sessions', async () => {
+        const manager = new Mock.SessionManagerMock();
+        await manager.startNew({
+          path: 'foo',
+          name: 'foo',
+          type: 'bar',
+          kernel: { name: Mock.DEFAULT_NAME }
+        });
+        expect(toArray(manager.running()).length).toBe(1);
+      });
+    });
+  });
+
+  describe('KernelSpecManagerMock', () => {
+    describe('.constructor', () => {
+      it('should create a new mock', () => {
+        const manager = new Mock.KernelSpecManagerMock();
+        expect(manager.isReady).toBe(true);
+      });
+    });
+
+    describe('.specs', () => {
+      it('should be the kernel specs', () => {
+        const manager = new Mock.KernelSpecManagerMock();
+        expect(manager.specs!.default).toBe(Mock.DEFAULT_NAME);
+      });
+    });
+
+    describe('.refreshSpecs()', () => {
+      it('should be a no-op', async () => {
+        const manager = new Mock.KernelSpecManagerMock();
+        await manager.refreshSpecs();
+      });
+    });
+  });
+
+  describe('ServiceManagerMock', () => {
+    describe('.constructor()', () => {
+      it('should create a new mock', () => {
+        const manager = new Mock.ServiceManagerMock();
+        expect(manager.isReady).toBe(true);
+      });
+    });
+
+    describe('.ready', () => {
+      it('should resolve', async () => {
+        const manager = new Mock.ServiceManagerMock();
+        await manager.ready;
+      });
+    });
+
+    describe('.contents', () => {
+      it('should be a contents manager', () => {
+        const manager = new Mock.ServiceManagerMock();
+        expect(manager.contents.normalize).toBeTruthy();
+      });
+    });
+
+    describe('.sessions', () => {
+      it('should be a sessions manager', () => {
+        const manager = new Mock.ServiceManagerMock();
+        expect(manager.sessions.isReady).toBe(true);
+      });
+    });
+
+    describe('.kernelspecs', () => {
+      it('should be a kernelspecs manager', () => {
+        const manager = new Mock.ServiceManagerMock();
+        expect(manager.kernelspecs.isReady).toBe(true);
+      });
+    });
+
+    describe('.dispose()', () => {
+      it('should be a no-op', () => {
+        const manager = new Mock.ServiceManagerMock();
+        manager.dispose();
+      });
+    });
+  });
+
+  describe('MockShellFuture', () => {
+    it('should create a new mock', async () => {
+      const msg = KernelMessage.createMessage({
+        channel: 'shell',
+        msgType: 'execute_reply',
+        session: 'foo',
+        username: 'bar',
+        msgId: 'fizz',
+        content: {
+          user_expressions: {},
+          execution_count: 0,
+          status: 'ok'
+        }
+      });
+      const future = new Mock.MockShellFuture(msg);
+      const reply = await future.done;
+      expect(reply).toBe(msg);
+      future.dispose();
+    });
+  });
+
+  describe('createFileContext()', () => {
+    it('should create a context without a kernel', async () => {
+      const context = Mock.createFileContext();
+      await context.initialize(true);
+      await context.sessionContext.initialize();
+      expect(context.sessionContext.session).toBe(null);
+    });
+
+    it('should create a context with a kernel', async () => {
+      const context = Mock.createFileContext(true);
+      await context.initialize(true);
+      await context.sessionContext.initialize();
+      expect(context.sessionContext.session!.kernel!.name).toBe(
+        Mock.DEFAULT_NAME
+      );
+    });
+  });
+});

+ 18 - 0
testutils/test/start_jupyter_server.spec.ts

@@ -0,0 +1,18 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import 'jest';
+
+const fetch = require('node-fetch');
+
+import { JupyterServer } from '../src';
+import { URLExt } from '@jupyterlab/coreutils';
+
+describe('JupyterServer', () => {
+  it('should start the server', async () => {
+    const server = new JupyterServer();
+    const url = await server.start();
+    await fetch(URLExt.join(url, 'api'));
+    await server.shutdown();
+  });
+});

+ 66 - 0
testutils/tsconfig.test.json

@@ -0,0 +1,66 @@
+{
+  "extends": "../tsconfigbase.test",
+  "include": ["src/*", "test/*"],
+  "references": [
+    {
+      "path": "../packages/apputils"
+    },
+    {
+      "path": "../packages/cells"
+    },
+    {
+      "path": "../packages/codeeditor"
+    },
+    {
+      "path": "../packages/codemirror"
+    },
+    {
+      "path": "../packages/coreutils"
+    },
+    {
+      "path": "../packages/docregistry"
+    },
+    {
+      "path": "../packages/nbformat"
+    },
+    {
+      "path": "../packages/notebook"
+    },
+    {
+      "path": "../packages/rendermime"
+    },
+    {
+      "path": "../packages/services"
+    },
+    {
+      "path": "../packages/apputils"
+    },
+    {
+      "path": "../packages/cells"
+    },
+    {
+      "path": "../packages/codeeditor"
+    },
+    {
+      "path": "../packages/codemirror"
+    },
+    {
+      "path": "../packages/coreutils"
+    },
+    {
+      "path": "../packages/docregistry"
+    },
+    {
+      "path": "../packages/nbformat"
+    },
+    {
+      "path": "../packages/notebook"
+    },
+    {
+      "path": "../packages/rendermime"
+    },
+    {
+      "path": "../packages/services"
+    }
+  ]
+}

+ 2 - 1
tsconfig.eslint.json

@@ -16,6 +16,7 @@
     "packages/**/stories/*",
     "scripts/*",
     "jupyterlab/*",
-    "jupyterlab/staging/*"
+    "jupyterlab/staging/*",
+    "testutils/**/*"
   ]
 }