Tuesday, December 20, 2016

Testing "this" with Jasmine

I had cause to test a function that references this in JavaScript and, although it ended up being easy, it took some figuring to get there.  Rather than have to figure it all out again (and maybe to save someone else some trouble, too) I'm putting my solutions here (yes, there are two solutions).

Here is the function I needed to test:
$scope.dataBound = function(e) {
    var data = this.dataSource.data();
    for (var i = data.length; --i >= 0;) {
        if (!data[i].IsBundle) {
            var row = $('#grid')
                .data('kendoGrid')
                .tbody.find('tr[data-uid="' + data[i].uid + '"]');
            $(row).find('td.k-hierarchy-cell .k-icon').removeClass();
        }
    }
};

The first - and easiest - solution is to set whatever you need on scope.  In Angular, when this is referenced, it's referring to scope so in my case I was using this.dataSource.data() so I was able to just set an object directly on scope called dataSource that had a data function.
it('should set this on scope', function() {
    // arrange
    scope.dataSource = {
        data: function() {
            return [];
        }
    };
    spyOn(scope.dataSource, 'data').and.callThrough();

    // act
    scope.dataBound();

    // assert
    expect(scope.dataSource.data).toHaveBeenCalledTimes(1);
});

The second - and in my opinion more correct - solution is to invoke the function using call and pass whatever you want this to be as your first parameter.
it('should set pass this using call', function() {
    // arrange
    var thisToSet = {
        dataSource: {
            data: function() {
                return [];
            }
        }
    };
    spyOn(thisToSet.dataSource, 'data').and.callThrough();

    // act
    scope.dataBound.call(thisToSet);

    // assert
    expect(thisToSet.dataSource.data).toHaveBeenCalledTimes(1);
});

They're pretty similar, but I prefer using call because it should work outside Angular as well.

Mozilla has a pretty sweet explanation of this and how to use it in case you want more detailed information.

Kendo UI Directives

Below is a list of directives found in the Kendo UI module.  I was trying to figure out how to set just the data of a data source on a grid and couldn't find a list.  I came across a question on SO asking for a list, but there was no answer.  I found another question on SO on how to list out the registered items in an Angular module, ran that against the kendo.directives module and voila!

This list is from the 2016.3.1118 release (please note that this is just a list of directives so if you're looking for something else you'll have to modify the code and get it yourself).

The code I used to generate the list:
   1:  angular.module('kendo.directives')['_invokeQueue'].forEach(function(value) {
   2:      if (value[1] === 'directive') {
   3:          console.log(value[2][0]);
   4:      }
   5:  });

The list:
2kendoAlert
2kendoAttribution
2kendoBarcode
2kendoBreadcrumbs
2kendoButton
2kendoCalendar
2kendoChart
2kendoConfirm
2kendoDiagram
2kendoDialog
2kendoDraggable
2kendoEditable
2kendoEditor
2kendoGantt
2kendoGrid
2kendoGroupable
2kendoMap
2kendoMenu
2kendoNavigator
2kendoNotification
2kendoPager
2kendoPopup
2kendoPrompt
2kendoReorderable
2kendoResizable
2kendoScheduler
2kendoSelectable
2kendoSlider
2kendoSortable
2kendoSparkline
2kendoSplitter
2kendoSpreadsheet
2kendoSurface
2kendoTooltip
2kendoTouch
2kendoUpload
2kendoValidator
2kendoWindow
kActionsheetContext
kAlign
kAllDayEventTemplate
kAltRowTemplate
kAltTemplate
kColumnHeaderTemplate
kDataCellTemplate
kDateHeaderTemplate
kDetailTemplate
kEditTemplate
kEmptyTemplate
kendoAutoComplete
kendoColorPalette
kendoColorPicker
kendoColumnMenu
kendoColumnSorter
kendoComboBox
kendoContextMenu
kendoDatePicker
kendoDateTimePicker
kendoDropDownList
kendoDropTarget
kendoDropTargetArea
kendoFileBrowser
kendoFilterCell
kendoFilterMenu
kendoFilterMultiCheck
kendoFlatColorPicker
kendoImageBrowser
kendoLinearGauge
kendoListView
kendoMaskedTextBox
kendoMediaPlayer
kendoMobileActionSheet
kendoMobileApplication
kendoMobileBackButton
kendoMobileButton
kendoMobileButtonGroup
kendoMobileCollapsible
kendoMobileDetailButton
kendoMobileDrawer
kendoMobileFooter
kendoMobileHeader
kendoMobileLayout
kendoMobileListView
kendoMobileLoader
kendoMobileModalView
kendoMobileNavBar
kendoMobilePane
kendoMobilePopOver
kendoMobilePopup
kendoMobileRecurrenceEditor
kendoMobileScroller
kendoMobileScrollView
kendoMobileScrollViewPage
kendoMobileShim
kendoMobileSplitView
kendoMobileSwitch
kendoMobileTabStrip
kendoMobileTimezoneEditor
kendoMobileView
kendoMultiSelect
kendoNumericTextBox
kendoPanelBar
kendoPivotConfigurator
kendoPivotFieldMenu
kendoPivotGrid
kendoProgressBar
kendoQRCode
kendoRadialGauge
kendoRangeSlider
kendoRecurrenceEditor
kendoResponsivePanel
kendoSearchBox
kendoSelectBox
kendoStaticList
kendoStockChart
kendoTabStrip
kendoTimePicker
kendoTimezoneEditor
kendoToolBar
kendoTreeList
kendoTreeMap
kendoTreeView
kendoViewTitle
kendoVirtualList
kendoVirtualScrollable
kendoZoomControl
kErrorTemplate
kEventTemplate
kHeaderTemplate
kIcon
kLinkTemplate
kMajorTimeHeaderTemplate
kMinorTimeHeaderTemplate
kRel
kRowHeaderTemplate
kRowTemplate
kSelectTemplate
kTemplate
kTransition

Thursday, December 1, 2016

Testing a Request in an ApiController

I recently came across a situation where I needed to test an action method on an ApiController to make sure the correct response was returned to the user based on the request.  In this particular case I was testing the ability to upload a file to an API and I needed to return a 400 (Bad Request) error if the content type of the request was not right.  I knew the code was working (I know, it wasn't TDD, but sometimes you have to roll with the punches), but I was having a hard time testing it.  To make things worse I couldn't use shims or fakes because the build server kept blowing up on them.  Fortunately, I came across a couple of really helpful blogs that pointed me in the right direction.

First, Shiju Varghese's blog on writing unit tests for an ApiController.  Next I used William Hallat's blog on testing a file upload to finish things off.

What I ended up with is pretty neat and easy to use, customized to meet my needs.  As usual, I'm posting it here so I don't have to redo the work next time.

The first key to making this work is declaring the controller.  Let's just say that our controller has a single object injected, an ILogger (a fairly common practice).  Instead of just doing this:
var controller = new ExampleController(_moqLogger.Object);

We want to do this:
   1:  var controller = new ExampleController(_moqLogger.Object)
   2:  {
   3:      Request = new HttpRequestMessage
   4:      {
   5:          Content = new ObjectContent(typeof(string), null, new JsonMediaTypeFormatter()),
   6:          Method = HttpMethod.Post
   7:      }
   8:  };

UPDATE: We actually also want to include an HttpConfiguration to prevent another error I was getting later:
   1:  var request = new HttpRequestMessage
   2:  {
   3:      Content = new ObjectContent(typeof(string), null, new JsonMediaTypeFormatter()),
   4:      Method = HttpMethod.Post
   5:  };
   6:  request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
   7:  var controller = new ExampleController(_moqLogger.Object)
   8:  {
   9:      Request = request
   8:  };

This declares that the request passed to the controller will actually be specified to contain content and a method (POST in this case).  Now that we have that, our tests won't fail with the awful (and unhelpful) "Object reference not set to an instance of an object" error you might be seeing.

In this case we've specified that the content will be a string, but we haven't specified any actual content.  But as I said before I needed to test whether the content was a file that had been uploaded, then also take certain actions based on that file.  To do that I actually needed to fake a file upload in the request itself.

A little bit of setup (this happens before each test run not each test):
   1:  [TestFixtureSetUp]
   2:  public void SetUpFixture()
   3:  {
   4:      using (var outFile = new StreamWriter(_testFile))
   5:      {
   6:          outFile.WriteLine("some test data");
   7:      }
   8:  }

The test:
   1:  [Test]
   2:  public void DoSomethingShouldReturnAnOkResult()
   3:  {
   4:      // arrange
   5:      var multipartContent = BuildFormDataContent();
   6:      multipartContent.Add(new StringContent("some value"), "someKey");
   7:   
   8:      var controller = new ExampleController(_moqLogger.Object)
   9:      {
  10:          Request = new HttpRequestMessage
  11:          {
  12:              Content = multipartContent,
  13:              Method = HttpMethod.Post
  14:          }
  15:      };
  16:   
  17:      // act
  18:      var response = controller.DoSomething();
  19:   
  20:      // assert
  21:      Assert.IsInstanceOf<OkResult>(response.Result);
  22:  }

The method called by the test:
   1:  private string _testFile = "test.file";
   2:   
   3:  private MultipartFormDataContent BuildFormDataContent()
   4:  {
   5:      var multipartContent = new MultipartFormDataContent("boundary=---011000010111000001101001");
   6:              
   7:      var fileStream = new FileStream(_testFile, FileMode.Open, FileAccess.Read);
   8:      var streamContent = new StreamContent(fileStream);
   9:      streamContent.Headers.ContentType = new MediaTypeHeaderValue("multipart/form-data");
  10:   
  11:      multipartContent.Add(streamContent, "TheFormDataKeyForTheFile", _testFile);
  12:   
  13:      return multipartContent;
  14:  }

And finally, the controller action method:
   1:  [HttpPost]
   2:  public async Task<IHttpActionResult> DoSomething()
   3:  {
   4:      if (!Request.Content.IsMimeMultipartContent("form-data"))
   5:      {
   6:          _logger.Information(() => "Unsupported media type");
   7:          return BadRequest("Unsupported media type");
   8:      }
   9:   
  10:      try
  11:      {
  12:          var root = @"C:\";
  13:          var provider = new MultipartFormDataStreamProvider(root);
  14:          await Request.Content.ReadAsMultipartAsync(provider);
  15:   
  16:          var someValue = provider.FormData.GetValues("someKey").FirstOrDefault();
  17:   
  18:          foreach (var file in provider.FileData)
  19:          {
  20:              var fileInfo = new FileInfo(file.LocalFileName);
  21:              // do something with the file here that returns a boolean
  22:              if(someOtherMethod()){
  23:                  return Ok();
  24:              }            
  25:   
  26:              return InternalServerError(new Exception("An error was encountered while processing the request"));
  27:          }
  28:   
  29:          return Ok();
  30:      }
  31:      catch (Exception ex)
  32:      {
  33:          return InternalServerError(ex);
  34:      }
  35:  }

This solved my problem and enabled me to test my action method on my controller.