0

I am trying out Stitch, a serverless/hosted JavaScript environment from MongoDB. My main purpose is to help me learn modern JavaScript, but I am trying to write a useful app as well.

I have written the following function, and saved it in my Stitch app. I believe this follows the documented way to write functions in Stitch, and I have tested it from the Stitch administration console:

exports = function(query){
  const http = context.services.get("HTTP");
  const urlBase = context.values.get("stackOverflowApiUrl");
  const options = [
    'order=desc',
    'sort=activity',
    'site=stackoverflow',
    'q=' + encodeURIComponent(query),
    'user=472495',
    'filter=!--uPQ.wqQ0zW'
  ];

  return http
    .get({ url: urlBase + '?' + options.join('&') })
    .then(response => {
      // The response body is encoded as raw BSON.Binary. Parse it to JSON.
      const ejson_body = EJSON.parse(response.body.text());
      return ejson_body.total;
    });
};

This code is pretty simple - it obtains an http object for making external API fetches, and obtains a configuration value for a URL urlBase to contact (resolving to https://api.stackexchange.com/2.2/search/excerpts) and then makes a call to the Stack Overflow Data API. This runs a search query against my user and returns the number of results.

So far so good. Now, I want to call this function locally, in Jest. To do this, I have installed Node and Jest in a local Docker container, and have written the following test function:

const callApi = require('./source');

test('Simple fetch with no user', () => {
    expect(callApi('hello')).toBe(123);
});

This fails, with the following error:

~ # jest
 FAIL  functions/callApi/source.test.js
  ✕ Simple fetch with no user (3ms)

  ● Simple fetch with no user

    TypeError: callApi is not a function

      2 | 
      3 | test('Simple fetch with no user', () => {
    > 4 |     expect(callApi('hello')).toBe(123);
        |            ^
      5 | });
      6 | 

      at Object.<anonymous>.test (functions/callApi/source.test.js:4:12)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.418s
Ran all test suites.

(In fact I was expecting it to fail, since it contains a global object context that Jest does not have access to. I will work out how to mock that later, but for now Jest cannot even see the function at all).

I suspect I can see the reason - in the Jest introduction docs, one has to do this for the SUT:

module.exports = function() { ... }

However the Stitch docs seem to require functions to be defined as:

exports = function() { ... }

I do not have a background in JavaScript to understand the difference. I could try module.exports in Stitch, but I would rather not, since this would either not work now, or cause a breakage in the future. Can Jest be instructed to "see" bare exports without the module prefix?

Incidentally, I have picked Jest because it is popular, and because some of my JavaScript colleagues vouch for it. However, I am not wedded to it, and would be happy to use something else if it is known to be better for Stitch development.

Update

Following the useful answer from jperl below, I find that the following construction is not possible in Stitch:

module.exports = exports = function() {}

I also cannot do this:

exports = function() {}
module.exports = exports

If I try either, I get the following error:

runtime error during function validation

So it looks like I have to get Jest to work without module.exports, or create a glue file that imports the exports version into module.exports, with the main file being used by Stitch, and the glue importer being used by Jest.

halfer
  • 19,824
  • 17
  • 99
  • 186

2 Answers2

1

I suggest you to read this thread. And you're right in thinking it has to do with modules.exports vs exports. The thing is that module.exports and exports first point to the same thing. So something like this works:

//modify the same object that modules.exports is pointing to
exports.a = {}
exports.b = {}

but this won't:

exports = {}

Why? Because now exports points to something else than module.exports so what you're doing has no effect at all.

Update

Following some updates in the comments, we came to the view that Stitch does not seem to support the export format that Jest requires.

halfer
  • 19,824
  • 17
  • 99
  • 186
jperl
  • 4,662
  • 2
  • 16
  • 29
  • Ah, thanks jperl! I wonder if my question is close enough to your linked question, I wonder if it could be called a duplicate. I was expecting the answer to be an implementation-specific oddity in Stitch, to be honest, and given that Stitch is a lesser-spotted beastie on Stack Overflow, I did not think it was possible that an answer could exist already. – halfer Dec 07 '19 at 18:47
  • "Because now exports points to something else than module.exports so what you're doing has no effect at all" - indeed, but Stitch can still import it. I wonder if this is because `exports` is read locally in the Stich environment, rather than being fetched by `require`? – halfer Dec 07 '19 at 18:49
  • I have updated the question to explain that `module.exports` is not accepted by Stitch. Is there some way that Jest can use a bare `exports`, short of me writing a test bootstrap that "pre-compiles" the tests to include the `module.` prefix? – halfer Dec 07 '19 at 18:58
  • @halfer Oh, interesting! That's a peculiar case. I'm guessing that module isn't defined at all and exports is not an alias of module.exports. – jperl Dec 07 '19 at 19:21
  • @halfer could you try adding `if (!module) module = {}; module.exports = exports`? – jperl Dec 07 '19 at 19:30
  • Thanks, suggestions of things to try are appreciated. Unfortunately I get the same response from the Stitch UI: `runtime error during function validation`. I wonder if they have a naive regex filter that only allows the narrow `exports = function() { }` format. – halfer Dec 07 '19 at 19:32
  • @halfer I've just read this: Require statements must appear inside a function declaration, not outside it. Stitch does not currently support statements in the global scope. – jperl Dec 07 '19 at 19:51
  • Thanks @jperl. I have a dim idea of what that means. In the terms that a non-JS person can understand, I think that means that Stitch won't bend to my requirements. Can I modify Jest configuration instead? I am minded to write a shell script to copy-and-modify all the tests, so that I can move forward. It is less than elegant, but if needs must `:=)`. – halfer Dec 07 '19 at 19:55
  • @halfer I mean, that's probably why you get an error whenever you try to add anything outside the function ('Stitch does not currently support statements in the global scope'). You're out of luck :D. I'm out of ideas. – jperl Dec 07 '19 at 19:59
  • No problem - and thank you. I will get my Bash script hacking hat on, and will see if Jest supports a pre-test bootstrap, so I can create exportable functions prior to Jest running the main tests. – halfer Dec 07 '19 at 20:54
0

This is an addendum to jperl's answer, to show how I got Jest working while respecting Stitch's limitations.

Firstly, it is worth noting how a Stitch application is laid out. This is determined by the import/export format.

auth_providers/
functions/
    function_name_1/
        config.json
        source.js
    function_name_2/
        config.json
        source.js
    ...
services/
values/

The config.json file is created by Stitch remotely, and is obtained through a export. This contains ID information to uniquely identify the function in the same folder.

I believe it is common JavaScript practice to mix tests with source code, so I am following that style (I am new to modern JS, and I confess I find this style untidy, but I am running with it nevertheless). Thus I add a source.test.js file in each function folder.

Finally, since there is a discrepancy between what Stitch requires and what Jest requires, I have written a script to create a source code file under _source.js in each function folder.

So, each folder will contain these files (the underscore files will probably be ignored by Git, as they will always be generated):

_source.js
config.json
source.js
source.test.js

In order to create the underscored copies, I am using this shell script:

#!/bin/bash

# Copy all source.js files as _source.js
for f in $(find functions/ -name source.js); do cp -- "$f" "$(dirname $f)/_$(basename $f)"; done

# Search and replace in all _source.js files
for f in $(find functions/ -name _source.js); do sed -i -e 's/exports =/module.exports =/g' $f; done

A bit hacky perhaps, but it works!

halfer
  • 19,824
  • 17
  • 99
  • 186