1.0.4 • Published 10 months ago

gcp-testing v1.0.4

Weekly downloads
-
License
ISC
Repository
-
Last release
10 months ago

Hello Everyone,

This package includes some utilities to do testing for your node project that uses firestore or google cloud storage and need some stubs!

Available stubs and how to use them

In some cases, you will need to have an online test, which is okay if you have a firebase testing project ready. But in most cases you don't actually need to make it online, that's why we're creating these stubs, to have the ability to emulate firestore functionalities offline. Currently, there exist these stubs:

Important Note : all stubs are created using sinon.js, for more examples and full API please refer to sinon stubs.

Firestore Get Stub

This function provides an Object with these properties :

  • queryGetStub stub for collection("...").where(...).get
  • docGetStub stub for collection("...").doc(...).get
  • transactionGetStub stub for transaction.get({...})
  • restore function that calls restore for all the stubs above
    • note that: stub.restore() remove the stub and restore the original function

How to use Let's have some example

  • Example #1
const { firestoreGetStub } = require("gcp-testing");

let collections = {}; 	// IMPORTANT
let getStub = firestoreGetStub(collections);

it("should return rate=0 in case of zero orders", async () => {

	// Create documents in Orders collection to be used in the test
	collections.Orders = [
		{user_id: 0, type: "order", returned: true},
		{user_id: 1, type: "order", returned: false},
		];


	const { orders, rate, returned } = await  aggregateReturnRate(2);
	// When aggregateReturnRate call collection.where.get
	// it won't call the real get
	// it will call our fake offline stub (getStub.queryGetStub)

	// Assertions
	assert.equal(orders, 0);
	....
	});

after(() => {
	// Remove "get" stub, i.e. real get function is back
	getStub.restore();
});

More on let collections = {}; // IMPORTANT Why this is important? this object should be at the test side and passed to the firestoreGetStub as it acts as our firestore collections, any change done to it will be seen by our get stub.. so, depending on your test case, create or remove documents and collections from it.

  • Example #2 What if we need to access the stubs to use its functions provided by sinon api? in this example we're going to access the transaction get stub.
let getStub;
let collections;

before() {
	collections = {}
	collections.Orders = {
		"one":{user_id:"xx", ...},
		"two":{user_id:"xx", ...},
		"three":{user_id:"xx", ...}
	}
	getStub = firestoreGetStub(collections);
}
after() {
	getStub.restore();
}
it("check testfunction for id '0'", async () => {
	await testFunction('0').then(() => {
	assert.isTrue(getStub.transactionGetStub.calledOnce, 'Called GET once');
	// Find out that the Orders collection is called in a document call
	assert.equal(
		getStub.transactionGetStub.getCall(0).firstArg._path.segments[0],
		"Orders"
	);
	})
})

As shown in the example, the return of firestoreGetStub is an Object, that consists of restore function which restores the original behaviour of all the get stubbed functions, transactionGetStub which is the stub for transcation get and we can access it and use its functions like calledOnce for example. Similar to transactionGetStub, there are queryGetStub and docGetStub.

Firestore Add Stub

This function provides a stub for collection("...").add

How to use Let's have an example

const { firestoreAddStub } = require("gcp-testing");

let addStub = firestoreAddStub(collections);

it("dummy example", () => {
	functionUnderTest();
	// When functionUnderTest call collection.add
	// it won't call the real add, it will call our fake offline stub

	// check if add stub is called
	assert.equal(
		addStub.calledOnce,
		true,
		"ُExactly One document should be added by this function"
		)

	// get the return of the add
	// (return is s a fake mimic of the real add return)
	let  doc = await  addStub.returnValues[0];
	// 0: first call return
	...
);
})


after(() => {
	// Remove "add" stub
	addStub.restore();
});

Firestore Transaction UPDATE Stub

This piece of code mimics a transaction update method. This means all calls like transaction.update(ref, object) will be overwritten. This will then use the collections object you provided and update the data inside the collections.

It should always be used in combination with the Transaction get Stub

How to use Let's have an example

let collections;
let getStub;
let updateStub;

before() {
	collections = {}
	collections.Orders = {
		"one":{user_id:"xx", keepoalas: 20, ...},
		"two":{user_id:"xx", keepoalas: 20, ...},
		"three":{user_id:"xx", keepoalas: 20, ...}
	}
	collections.Aggregation = {
		"xx":{
			keepoalas:{
				keepoalas: 10
			}
		}
	}
	getStub = getStub(collections);
	updateStub = firestoreTransactionUpdateStub(collections);
}
after() {
	getStub.restore();
    updateStub.restore();
}
it("check aggregation for single user", async () => {
          testFunction('xx').then(() => {
            assert.isTrue(currentObject.getStub.transactionGetStub.calledOnce, 'Called GET once');
            assert.isTrue(
              currentObject.updateStub.calledOnce,
              'Called Update once',
            );
			// should aggregate all Keepoalas from collection "Orders"
            assert.equal(
              currentObject.collections.Aggregation['xx'].keepoalas.keepoalas,
              60,
              'keepoalas',
            );
          });

})

Firestore Transaction SET Stub

Similar to Transaction UPDATE Stub but it will overwrite the document if it exist.

setStub = firestoreTransactionSetStub(this.collections);

Stub for auth users

Not to mess with the authentication library, we created a stub for admin.auth().getUser(<userid>) that allows you to add your own users

Users look like this:

{
  "uid": "XXXXX",
  "email": "test@test.de",
  "displayName": "Test User"
}

You can create an object with multiple of these and hand them over, let's have an example:

before() {
	this.users = {
      0: {
        uid: '0',
        email: 'test@firebase.com',
        displayName: 'TestName',
      },
      1: {
        uid: '1',
        email: 'test1@firebase.com',
        displayName: 'Test1Name',
      },
    };
	this.getUserStub = firebaseAuthStub(this.users);
}
after() {
	this.getUserStub.restore()
}

This will simply overwrite all calls like

return admin.auth().getUser(user_id).then((userRecord) => ...);

and return the userRecord from your collection instead of the real one

Google Storage Buckets Stub

You can find stubbing for google cloud storage buckets in the testing directory (node_modules/gcp-testing/googleCloudStorageStub.js) , the stub is faking the function call Storage.bucket() to make it return a fake bucket object that mimics the behavior of the original object but it uses the local file system instead.

How To Use

// Import the stub factory function

const {getBucketStub} = require('gcp-testing');
// Start stubbing
const bucket_stub = getBucketStub();

/**
 * Now anything is done using the object returned from Storage.bucket()
 * Is being processed on the local filesystem not the real cloud storage
 */

const {Storage} = require('@google-cloud/storage');

const storage = new Storage({
  projectId: 'my_project',
});

const bucket = storage.bucket('my_bucket');

// Example 1: write ('my_file.txt') into local bucket ('my_bucket') in my_project
// =================================================================
const tempLocalFile = path.join(os.tmpdir(), 'my_file.txt');
fs.writeFileSync(tempLocalFile, 'this is my file!');

bucket
  .upload(tempLocalFile, {
    metadata: {
      metadata: {
        title: 'hello',
      },
    },
  })
  .then(
    () => console.log('uploaded successfully!'), // uploaded successfully!
  );

// Example 2: read ('my_file.txt') file and its metadata from local bucket ('my_bucket') in my_project
// Note: this can be done inside a test to assert file contents
// =================================================================

// I. read file
bucket
  .file('my_file.txt')
  .download()
  .then((file_contents) => {
    assert.equal(file_contents.toString(), 'this is my file!');
  });

// II. read metadata
bucket
  .file('my_file.txt-metadata.json')
  .download()
  .then((file_contents) => {
    const actual_metadata = JSON.parse(file_contents.toString());
    const expected_metadata = {metadata: {title: 'hello'}};
    assert.deepStrictEqual(actual_metadata, expected_metadata);
  });

// =================================================================

// Note that the file is read from the local filesystem:
// 'node_modules/gcp-testing/storage.googleapis.stub/keepoalatesting/my_bucket/my_file.txt'
// Also writing a file using either upload or write stream will write the file in the same path

// =================================================================

// Remove the stub (return back to the original cloud storage)
bucket_stub.restore();

Currently Supported Features

Notes

  • The local file sytem is structured similar to that of google cloud storage, such that the storage is located at the directory node_modules/gcp-testing/storage.googleapis.stub, and inside this directory there should be a directory for each bucket.
  • For each file written to the local storage another file is created store its metadata, which is named after the original file name + -metadata.json, for example if a file called myfile.txt is created in bucket mybucket another file is created inside the bucket mybucket with the name myfile.txt-metadata.json. note that the metadata file is always created even if no metadata is specified (in that case the json file will have an empty object only {}).

Logs

functions.logger.log is stubbed to make the logger silent. the utility module logger.js is the one required to stub the logger, inside index.test.js you will realize that it's imported and being used.

By default, all logs in your test will be silent, but you can make use of them.

  • Import logger
const {logger} = require('gcp-testing');
  • Flush the logger before your test to remove all logs from previous tests
before(()=> {
    logger.flush();
})
  • Print all the logs at the end of the test
after(() => {
    console.log('--- mytest LOGS ---');
    logger.print();
});
  • Access the logs
logger.logs.forEach(log => ...);

logs is in this format [{type: string, text: string }] and type can be: warn, log, error,... (i.e. functions.logger.type)

  • To start stubbing and restore the original functions
// start stubbing
logger.stub();
// restore original (i.e. stop stubbing)
logger.restore();

NOTE: in index.test.js logger is already imported and both logger.stub() & logger.restore() are called globally before start and after finish all testing respectively, i.e. you won't need to use these two functions.

NOTE: if you separated your test from index.test.js, make sure to pass the logger object to your test from index.test.js.