Monday, February 4, 2019

Mocking with Google Application Scripts (Javascript)

This article covers why you need to do mocking and how to do it using examples using Google Application Script (a form of Javascript).  In each case, we'll start with a code example (not following the TDD so we can focus on mocking) then write a micro test. From there, we'll focus on the steps to Implement a Mock Object:
  1. find a way to inject the mock
  2. design a minimal mock object that achieves the necessary goals
  3. use the mock in an automated micro test

What is Mocking

Mocking is the practice of using simple objects in place of *real* objects in order to have full control of product code for which you want to write an automated test.  (Product code is the code you put into production as it gives you the functionality you or your users want.  Test code is Javascript code that tests the product code.)

Because Javascript is loaded and executed at runtime and isn't strict about types, it is an extremely flexible language. Yet, when writing micro tests there will be code that correctly does its job in production, yet needs to be mocked or return fake data for unit testing.

Situations that require a Mock object

Mock objects are used in place of real objects are doing the following "no nos."

Micro Testing No Nos

  • write to our computer screen (ui widgets)
  • communicate with a network (databases, webservices, cloud services,...)
  • connect to a system service such as the system clock
  • operate the file system
  • in general: bring complication to your micro test that slows it down or makes it reliant on something at adds complexity.

Example:

Copy the below code into a GAS script file called Alert.gs:
function showAlert() {
  var ui = SpreadsheetApp.getUi(); // Same variations.
  var result = ui.alert(
     'Please confirm',
     'Are you sure you want to continue?',
      ui.ButtonSet.YES_NO);
  // Process the user's response.
  if (result == ui.Button.YES) {
    // User clicked "Yes".
    ui.alert('Confirmation received.');
  } else {
    // User clicked "No" or X in the title bar.
    ui.alert('Permission denied.');
  }
}

Run the function and you'll get the following in the script editor:
Note the script editor's prompt about: "Cancel Dismiss." What has happened is the widget is showing in the Google Docs application to which the script is attached.  In this case, I clicked on the tab for the Google Sheet.
Take a look at the above and think about what is important to check with an automated test. For this type of functionality, the below is a typical list of "checks" or test case:

Checks for Alert

  1. The prompt text correct. In this case, the prompt is "Please confirm."
  2. The question you're asking the user is correct. In this case "Are you sure you want to continue?"
  3. the dialog box is of the correct type.  In this case, has a Yes and No.
  4. When Yes is clicked, a "yes message" is returned to the caller.  When No is clicked, a "no message" is returned to the caller.

If we test the above checks in an automated micro test, we avoid paying the biggest (by 80%) cost of software development, the cost of maintenance.
  • No manual labor is used every iteration to confirm that this code is working as designed.
  • It takes essentially no time to execute a test.
  • We can use this test countless of times before shipping our product to confirm that everything is working as we designed it.
  • No manual labor will be necessary to later debug the code to track down why it stopped working.
  • The micro test acts as documentation of our code. And if we break our code, our documentation will tell us there is a problem.
Create the following micro test in AlertTest.gs. Google Application Script Test GAST is used in this article (you'll need to install it or copy and paste the library into a script in your GAS project). It's an alright framework whose main advantage is that it's simple. QUnit for GAS is a more "feature-ful" alternative for doing Google Application Script (GAS) work. (Due to a unique situation, simpler was better for the GAS development I was involved with.)
function alertTest(){
  test('methodToTest and the test scenario.', function(assert){
    // Arrange what we must to get the test ready
    // Act on the code that must be tested
    // Assert what the results should be 
  })
}
Run allertTest and check the logs (in the scripts editor select View->Logs) for the output.  You should see the following:
 If the above is essentially you see, then your environment is in good shape.  Now we start doing the  three steps to implementing a mock object.

Step 1, find a way to inject a mock

The fragment from Alert.gs has our mocking target highlighted.
function showAlert() {
  var ui = SpreadsheetApp.getUi(); 
...
Our code is using the SpreadsheetApp global object (a Singleton) could be a problem.  So I write down on a piece of paper: worry about SpreadsheetApp.  Since we don't know if this is a big deal yet, we continue reading the code.
  var result = ui.alert(
     'Please confirm',
     'Are you sure you want to continue?',
      ui.ButtonSet.YES_NO);
Now we find our UI code which is what we want to test.  So the question in my mind is: how to inject a mock ui object here?  It's being accessed via a variable called "ui."  Where did "ui" come from?


  var ui = SpreadsheetApp.getUi(); 
It came from our Singleton.  OK.  On my paper I underline SpreadSheetApp and add the following comment.
  var ui = SpreadsheetApp.getUi();  // inject a mock for UI here!
Great!  Now how to do it?  It turns out that javascript being a dynamic language lets us do this in many ways. Simply refactoring the code to the following will not bother any of the callers of the showAlert function.
function showAlert(uiMock) {
  var ui = (uiMock == null ? SpreadsheetApp.getUi() : uiMock); 
...
 As with any refactoring, you should test it. Since we don't yet have a micro test you'll need to do a manual test. Execute showAlert function, switch back to the Google Sheet you embedded the script into so you can see the dialog and click "yes" or "no."

Step 2, Design a minimal mock object that achieves the necessary goals 

We don't want our micro tests to be anywhere as complicated as our product code, so they must test only one scenario and do it as simply as possible. It's best to not need mock objects at all, but we need SOMETHING to take place of the real UI object which SpreadSheetApp normally returns.
So we are committed to that. Now what would be the minimal functions that this mock object must provide?  Go read the code and see what is called upon the ui variable.
  var result = ui.alert(
     'Please confirm',
     'Are you sure you want to continue?',
      ui.ButtonSet.YES_NO);

  // Process the user's response.
  if (result == ui.Button.YES) {
    // User clicked "Yes".
    ui.alert('Confirmation received.');
  } else {
    // User clicked "No" or X in the title bar.
    ui.alert('Permission denied.');
  }
}
On a piece of paper I write down the items bolded above. Our mock ui needs to have a function for alert, return a ButtonSet property, and a Button property. These are things we need to put into the Arrange section of our micro test. We are ready to implement our first micro test case.

Step 3, use the mock in an automated micro test

Let's build this micro test capability one simple test case at a time.  (Each function named "test" is a "test case.") Among our list of Checks for Alert, let's select the first, which is to check the prompt text.  Roughly, the following is what we want to do.
function alertTest(){
  test(' ', function(assert){
    // Arrange what we must to get the test ready
    var mockUI = {}
    // Act on the code that must be tested
    showAlert(mockUI)
    // Assert what the results should be
    // assert.equal(, 'Please confirm','')
  })
}
To finish what's bolded, the assert line, we need to get our mock object to "spy on" what happens when showAlert does the highlighted section.
 We can implement a spy in our mock object thusly and finish up our assert:
Update the first argument to the test function (method_to_test and test scenario) with what we are testing: 'showAlert prompt message is correct.'
Go back and look at our list of items that are called upon our ui variable. Although we've only implemented one of the three, simply run the micro test and discover via the error message or log to see what that function needs.  (I run the micro test now rather than assume my list is correct because sometimes I discover something new.)
I confirm it does need the ButtonSet implemented, and so I add it.
Running the test again fails with a TypeError because it's trying to read someone's YES property.  The logs reveal the "someone" is on line 10 of Alert which for me is the following:

So we add the "Button" property to our mock object.
Run the test and now it passes.  To confirm it is working correctly, inject a bug by change the highlighted line in the product code and observe the test fail:


Go ahead and remove the bug, run the test and observe it passes.

Continue and Share

Go ahead and implement all the checks for this test by adding more asserts and spies.  At some point you'll need to add another "test" function because the "arrange" section you have will be focused on one scenario and you'll need to "arrange" a different scenario for some of the checks.

Share your solution and I'll be happy to give you feedback.  Take a picture of the test automation you built and tweet it to: @LancerKind.  I'll give feedback.  Go for it because your solution may be better than mine.

Cheers,
Coach






No comments:

Post a Comment