4

I've got a typescript module that uses the browser's native Blob. I'd like to test it in node so I need that same module to see a global fake Blob implementation. A very simple fake blob implementation will do for me

class Blob {

  parts?: any;
  options?: any;

  constructor(parts: any, options?: any) {
    this.parts = parts;
    this.options = options;
  }

  getType():string {
    return options.type;  // I know, hacky by just a demo
  }
}

But how do I inject it into the global namespace of node so that my code that normally runs in the browser will see it when I run the tests in node?

In other words imagine I have a class that's expected to return a native browser Blob

export class BlobMaker {
  static makeTextBlob(text):Blob {
    return new Blob([text], {type: 'text/text'});
  }
}

And now I want to test it in node (not in the browser) as in

import { BlobMaker } from '../src/blob-maker';
import * as expect from 'expect';

describe('BlobMaker', () =>  {
  it('makes a text blob', () => {
    const blob = BlobMaker.makeTextBlob('foo');

    expect(blob.getType()).equalTo('text/text');
  });
});

This fails to compile because Blob doesn't exist in node.

Apparently I can claim it exists by adding?

export interface Global extends NodeJS.Global {
  Blob: Blob;
}

But I still need to inject my fake Blob class above so that the code I'm testing will use it. Is there a way to do that or am I supposed to solve this some other way?

It doesn't seem like I can abstract Blob into some kind of interface since I need the signature of makeTextBlob to be an actual native browser Blob and not some custom type.

I guess I get that I could pass a Blob factory deep into my library from the tests. Passing a Blob factory that deep in seems like overkill. I actually tried this by typescript complained. I think because it thinks the type being returned by BlobMaker.makeBlob is different

Here's that code

let makeBlob = function(...args):Blob {
   return new Blob(...args);
};

export function setMakeBlob(fn):void {
   makeBlob = fn;
};

export class BlobMaker {
  static makeTextBlob(text):Blob {
    return makeBlob([text], {type: 'text/text'});
  }
}

Then in the tests

import { BlobMaker, setMakeBlob } from '../src/blob-maker';
import { Blob } from "./fake-blob';
import * as expect from 'expect';

describe('BlobMaker', () =>  {
  it('makes a text blob', () => {
    setMakeBlob(function(parts, options) {
      return new Blob();
    });

    const blob = BlobMaker.makeTextBlob('foo');

    expect(blob.getType()).equalTo('text/text');   // ERROR!
  });
});

I get an error there is no getType method on Blob. I'm guessing TS thinks the Blob returned by BlobMaker.makeTextBlob is the native one. I tried casting it

    const blob = BlobMaker.makeTextBlob('foo') as Blob;

But it didn't like that either.

Effectively this seems like it would all be solved if I could just inject my Blob class into the global namespace in node. So, how do I do that?

gman
  • 100,619
  • 31
  • 269
  • 393
  • I was under the impression that `/// – Patrick Roberts Jul 12 '18 at 04:42
  • Sorry this is my first typescript experience but how would that help BlobMaker find `Blob`? – gman Jul 12 '18 at 04:49
  • Oh wait I misread, your `Blob` is a `class`, not an `interface` (which _should_ mean it's in a normal `.ts` file. Your `BlobMaker` module should `import Blob from './blob'` or wherever it's exported from. Even though it's understood to be a global variable, it's much easier to deal with it as part of a module when using TypeScript. – Patrick Roberts Jul 12 '18 at 04:56
  • I'm not sure how that helps. This code still needs to run in the browser and return a browser native `Blob` (updated the question). I just want to test it with a fake blob in tests running in node. – gman Jul 12 '18 at 05:17
  • I'm not particularly well-versed in TypeScript yet myself, but looking at the context here, I think you should look into mocking the `Blob` module. I found [this answer](https://stackoverflow.com/a/38414108/1541563) which may or may not be useful to you, but what you can do is rewrite the file that `Blob` is in to attach to `global` only if nothing is already defined on `global.Blob`. – Patrick Roberts Jul 12 '18 at 05:58
  • Combining my suggestion with the linked answer, maybe something like `spyOn(global, 'Blob').andReturn('./blob')` or something similar during your test. – Patrick Roberts Jul 12 '18 at 06:01

1 Answers1

1

So this is how I solved it. Hope it's not too horrible

First according to this Q&A I can extend the global object in node by adding a separate file. In my case I made test.d.ts and put this inside

declare namespace NodeJS {
    interface Global {
        Blob: any
    }
}

Then in my test I did this

import { BlobMaker } from '../src/blob-maker';
import * as expect from 'expect';

class Blob {

  parts?: any;
  options?: any;

  constructor(parts: any, options?: any) {
    this.parts = parts;
    this.options = options;
  }

  getType():string {
    return options.type;  // I know, hacky by just a demo
  }
}

describe('BlobMaker', () =>  {
  it('makes a text blob', () => {

    global.Blob = Blob;

    const blob = BlobMaker.makeTextBlob('foo');

    expect(blob.getType()).equalTo('text/text');
  });
});

and it's working. I should probably move the part that's patching global.Blob to another module but it at least suggests a solution.

gman
  • 100,619
  • 31
  • 269
  • 393
  • That works but just to make sure it doesn't potentially interfere with other tests, I'd be more explicit about the mocking by rewriting `describe('BlobMaker', () => { const { Blob } = global; beforeEach(() => { global.Blob = require('./blob'); }); afterEach(() => { global.Blob = Blob; }); it(...); });` – Patrick Roberts Jul 12 '18 at 06:05