Monday, February 27, 2012

A primer on unit testing with Moq

The following code is a primer on unit testing, just get Moq from Nuget. Note: Actual unit testing should be done on a separate project, not on Main method :-) This code just demonstrates how elegant Moq is for unit testing needs.

using System;

using Moq;

namespace MoqSample
{
    class Program
    {
        static void Main(string[] args)
        {
            // ARRANGE
            
            decimal amount = 100;

            // that's why you will love unit testing, mishaps could be avoided :-)
            string errorMessage = "Could be an anomaly, passed amount is different from amount to transfer"; 

            var mockedSource = new Mock<IAccount>();
            mockedSource.Setup(x => x.Withdraw(It.Is<decimal>(a => a != amount))).Throws(new Exception(errorMessage));
            mockedSource.Setup(x => x.Deposit(It.Is<decimal>(a => a != amount))).Throws(new Exception(errorMessage));
            
            
            var mockedDest = new Mock<IAccount>();
            mockedDest.Setup(x => x.Withdraw(It.Is<decimal>(a => a != amount))).Throws(new Exception(errorMessage));
            mockedDest.Setup(x => x.Deposit(It.Is<decimal>(a => a != amount))).Throws(new Exception(errorMessage));
            



            // ACT

            int test = 0;
            /*
             * 0's error: NONE
             * 
             * 1's error:
             * Expected invocation on the mock should never have been performed, but was 1 times: source => source.Deposit(.amount)
             *  
             * 2's error:
             * Expected invocation on the mock once, but was 2 times: dest => dest.Deposit(.amount)
             * 
             * 3's error:
             * Expected invocation on the mock once, but was 0 times: dest => dest.Deposit(.amount)
             * 
             * 4's error:
             * Expected invocation on the mock once, but was 0 times: source => source.Withdraw(.amount)
             * 
             * 5's error:
             * Could be an anomaly, passed amount is different from transfer amount
             *             
             * */

            if (test == 0)
                TransactionMaker.TransferFund(mockedSource.Object, mockedDest.Object, amount);                
            else if (test == 1)
                TransactionMaker.TransferFundBuggy1(mockedSource.Object, mockedDest.Object, amount);                
            else if (test == 2)
                TransactionMaker.TransferFundBuggy2(mockedSource.Object, mockedDest.Object, amount);
            else if (test == 3)
                TransactionMaker.TransferFundBuggy3(mockedSource.Object, mockedDest.Object, amount);
            else if (test == 4)
                TransactionMaker.TransferFundBuggy4(mockedSource.Object, mockedDest.Object, amount);                
            else if (test == 5)
                TransactionMaker.TransferFundAnomalous(mockedSource.Object, mockedDest.Object, amount);                
            else
                throw new Exception("Select test case from 0 to 5");


            // ASSERT

            mockedSource.Verify(source => source.Withdraw(amount), Times.Once());
            mockedSource.Verify(source => source.Deposit(amount), Times.Never());

            mockedDest.Verify(dest => dest.Deposit(amount), Times.Once());
            mockedDest.Verify(dest => dest.Withdraw(amount), Times.Never());

        }

        
    }


    public static class TransactionMaker
    {

        public static void TransferFund(IAccount sourceAccount, IAccount destAccount, decimal amountToTransfer)
        {
            sourceAccount.Withdraw(amountToTransfer);
            destAccount.Deposit(amountToTransfer);            
        }

        public static void TransferFundBuggy1(IAccount sourceAccount, IAccount destAccount, decimal amountToTransfer)
        {
            sourceAccount.Withdraw(amountToTransfer);
            sourceAccount.Deposit(amountToTransfer);            
        }

        public static void TransferFundBuggy2(IAccount sourceAccount, IAccount destAccount, decimal amountToTransfer)
        {
            sourceAccount.Withdraw(amountToTransfer);
            destAccount.Deposit(amountToTransfer);
            destAccount.Deposit(amountToTransfer);
        }


        public static void TransferFundBuggy3(IAccount sourceAccount, IAccount destAccount, decimal amountToTransfer)
        {
            sourceAccount.Withdraw(amountToTransfer);            
        }

        public static void TransferFundBuggy4(IAccount sourceAccount, IAccount destAccount, decimal amountToTransfer)
        {
            sourceAccount.Deposit(amountToTransfer);
        }

        public static void TransferFundAnomalous(IAccount sourceAccount, IAccount destAccount, decimal amountToTransfer)
        {
            sourceAccount.Withdraw(amountToTransfer);
            destAccount.Deposit(amountToTransfer + 0.5M);
        }

        
    }



    public interface IAccount
    {
        void Withdraw(decimal v);
        void Deposit(decimal v);
    }

    public class Account : IAccount
    {
        public void Withdraw(decimal amount)
        {
        }

        public void Deposit(decimal amount)
        {
        }
    }
}

Monday, January 23, 2012

Liskov Substitution Principle, a simple example

The Liskov Substitution Principle of object oriented design states:

In class hierarchies, it should be possible to treat a specialized object as if it were a base class object.


using System;

namespace Liskov
{
 class Rectangle
 {    
  int _height;
  public virtual int Height
  {
   get{
    return _height;
   }
   set {
    _height = value;
   }
  }
  
  int _width;
  public virtual int Width
  {
   get {
    return _width;
   }
   set {
    _width = value;
   }   
  }
  
  public int Area
  {
   get {
    return _width * _height;
   }
  }
  
  
  public Rectangle (int width, int height)
  {
   Height = height;
   Width = width;
  }
  
 }
 
 
 class Square : Rectangle
 {
  public override int Height {
   set {
    base.Height = value;
    base.Width = value;
   }
  }
  
  public override int Width {
   set {
    base.Width = value;
    base.Height = value;
   }
  }
  

  public Square (int side) : base(width: side, height: side)
  {
  }
  
 }
 
 class MainClass
 {
  public static void Main (string[] args)
  {
   Rectangle s = new Square(side: 10);
   
   // 100, correct
   Console.WriteLine ("Area: {0}", s.Area); 
   
   // reusing the object r as rectangle object     
   s.Height = 4; 
   s.Width = 3;
   
   // expecting 12, actual output is 9. incorrect. Liskov Substitution Principle is violated
   Console.WriteLine ("Area: {0}", s.Area); 
  }
 }
}

The code above violates Liskov Substitution Principle, there should be no side effects on results even I use Square object for Rectangle purposes. Don't override properties just because it look smart to do so.

To set the Square's sides properly, set the two sides via one property only:

public int Side {
 get {
  return Height;
 }
 set {
  Height = Width = value;
 }
}



Web Sharing not working with mod_mono

If Web Sharing is not working after you upgraded to Lion, and you happen to configured ASP.NET Mono on your Snow Leopard before. Just do these 5 steps:

1. download the latest mod_mono Apache module

2. extract the content of the archive

3. do these on Terminal:

./configure
make
sudo make install

4. Click Web Sharing

5. No step five. Voila! Happy .NET computing on Mac and Mono again


No tedious steps taken, if the above steps doesn't work for you. Try this:

http://stackoverflow.com/questions/8815403/trouble-installing-mod-mono-on-mac-osx-lion


How to rule out that it is not a problem on Apache per se (most articles explain how to fix Apache itself) ? Use sudo apachectl configtest. Using that, I saw error message on httpd.conf regarding Mono, that makes googling easier for me, I narrowed down my search, and arrived on stackoverflow's solution.

P.S. I forgot to copy the exact mod_mono error on httpd.conf, it's something like: syntax error on httpd.conf dlopen mod_mono.so

Sunday, November 27, 2011

Flexigrid CRUD(inline form) with ASP.NET MVC

In this post, I'll show you the supporting code for placing a form within flexigrid.


First, declare a variable for the html string of your form:

<script type="text/javascript">
    var formHtml = "";
    

    function canRender() {
        return _canRender;
    }

    $(function () {
        _canRender = false;
    });

</script>


We use canRender so we can defer the execution of javascript of the form's html string

<div id="editor" style="visibility: hidden">
    @using (Html.BeginForm(null, null, FormMethod.Post, new { id = "theForm" }))
    {
        <fieldset>
            <legend>Person</legend>

            @Html.HiddenFor(x => x.PersonId)
            @Html.HiddenFor(x => x.RowVersion)

            <div class="editor-label">
                @Html.LabelFor(x => x.Username)
            </div>
            <div class="editor-field">
                @Html.EditorFor(x => x.Username)
                @Html.ValidationMessageFor(x => x.Username)
            </div>


            <div class="editor-label">
                @Html.LabelFor(x => x.Firstname)
            </div>
            <div class="editor-field">
                @Html.EditorFor(x => x.Firstname)
                @Html.ValidationMessageFor(x => x.Firstname)
            </div>

            <div class="editor-label">
                @Html.LabelFor(x => x.Lastname)
            </div>
            <div class="editor-field">
                @Html.EditorFor(x => x.Lastname)
                @Html.ValidationMessageFor(x => x.Lastname)
            </div>


            <div class="editor-label">
                @Html.LabelFor(x => x.Country.CountryId)
            </div>
            <div class="editor-field">

                <table style="border-style: none">
                <tr>
                <td style="border-style: none">
                @Html.AjaxComboBoxFor(x => x.Country.CountryId, "/Countries/Management/Lookup/", "/Countries/Management/Caption/",
                    new { }, new { sub_info = true, can_render_callback = "canRender", textbox_width = 200 })    
                </td>
                <td style="border-style: none">
                @Html.ValidationMessageFor(x => x.Country.CountryId)
                </td>
                </tr>
                </table>                     
            </div>
            

            <div class="editor-label">
                @Html.LabelFor(x => x.FavoriteNumber)
            </div>
            <div class="editor-field">
                @Html.EditorFor(x => x.FavoriteNumber)
                @Html.ValidationMessageFor(x => x.FavoriteNumber)                
            </div>

            <p>
                <input type="submit" value="Save" />
                <input type="button" id="Closer" value="Close" />
            </p>


            
        </fieldset>
        
        <div style="max-width: 500px; width: 500px;">
            @Html.JsAccessibleValidationSummary(excludePropertyErrors: true)            
        </div>
    }
<script type="text/javascript">

    $(function () {


        if (!canRender()) return;


        var scope = $('#theForm');
        parseDynamicContent(scope);




        $('#Closer', scope).click(function (e) {
            closeForm($(scope));
        });

        $('input[type=submit]', scope).click(function (e) {
            try {


                e.preventDefault();

                if (!scope.valid()) {
                    // alert('has invalid');
                    return;
                }

                save(scope);

                // closeForm(scope);
            } catch (e) {
                alert("Error " + e);
            }

        });

        $(scope).attr('id', guid());
    });

    function save(scope) {        

        $.ajax({
            url: '/People/Management/SaveViaAjax',
            type: 'POST',
            data: $(scope).serialize(),
            success: function (result) {

                var isOk = $(scope).modelValidation(result);


                if (isOk) {

                    var isNew = $('#PersonId', scope).val() == '';

                    if (isNew) {
                        $('#PersonId', scope).val(result.PersonId);
                    }

                    $('#RowVersion', scope).val(result.RowVersion);


                    if (isNew) {
                        $(scope).closest('table').flexReload();
                    }
                    else {


                        setFgEditText(scope, 'Username', $('#Username', scope).val());
                        setFgEditText(scope, 'Firstname', $('#Firstname', scope).val());
                        setFgEditText(scope, 'Lastname', $('#Lastname', scope).val());
                        setFgEditText(scope, 'FavoriteNumber', $('#FavoriteNumber', scope).val());

                        setFgEditText(scope, 'Country', $('#Country_CountryId', scope).ajc().getText());

                        closeForm(scope);
                    }

                }


                // $('#firstTable').flexReload();



            },
            error: function (a, b, c) {
                alert(a.statusText);
                alert(b);
                alert(c);
            }
        });                       //ajax
    }//save

</script>
</div>


Here's the flexigrid setup and getting the form's html string:

<script type="text/javascript">


    $(function () {
        // main..
        setupForm();

        setupFirstTable();
        

        
        // ..main
    });


    function setupForm() {
        formHtml = $('#editor').html();
        $('#editor').remove();        
    }

    function setupFirstTable() {
        $('#firstTable').flexigrid({
            url: '/People/Management/List',
            dataType: 'json',
            colModel: [
                    { display: 'User', name: 'Username', width: 150, sortable: true, align: 'left' },
                    { display: 'Firstname', name: 'Firstname', width: 150, sortable: true, align: 'left' },
                    { display: 'Lastname', name: 'Lastname', width: 150, sortable: true, align: 'left' },
                    { display: 'Favorite#', name: 'FavoriteNumber', width: 150, sortable: true, align: 'left' },
                    { display: 'Country', name: 'Country', width: 150, sortable: true, align: 'left' },
                    { display: 'RowVersion', name: 'RowVersion', width: 150, sortable: true, align: 'left', hide: true }
                ],

            buttons: [
                    { name: 'Add', bclass: 'add', onpress: add },
                    { separator: true },
                    { name: 'Edit', bclass: 'edit', onpress: edit },
                    { separator: true },
                    { name: 'Delete', bclass: 'delete', onpress: del }
                ],

            singleSelect: true,
            sortname: 'Lastname',
            sortorder: 'asc',
            usepager: true,
            title: 'Persons',
            useRp: true,
            rp: 5,
            rpOptions: [5, 10, 15, 20, 25, 40],
            showTableToggleBtn: true,
            width: 900,
            height: 'auto',
            preProcess: function (data) {
                var rp = getFgRowsPerPage($('#firstTable'));
                for (i = data.rows.length; i < rp; ++i) {
                    data.rows.push({ 'id': '', 'cell': ['', '', '', '', '', ''] });
                }                
                return data;
            }
        });    // flexigrid

        setupGrid($('#firstTable'));

    } //setupFirstTable



    function add(com, grid) {
        try {            
            closeFormByGrid(grid);            
            showAddFormByGrid(grid, formHtml);            
        } catch (e) {
            alert('error ' + e);
        }
    }


    function edit(com, grid) {

        closeFormByGrid(grid);


        var items = $('.trSelected', grid);
        var item = items[0];
        var pk = item.id.substr(3);

        if (pk.length == 0) return;

        

        $.ajax({
            url: '/People/Management/GetUpdated/' + pk,
            type: 'POST',
            success: function (data) {



                showEditForm(item, formHtml, function () {

                    var form = $('form', grid);


                    $('#PersonId', form).val(data.Record.PersonId);
                    $('#Username', form).val(data.Record.Username);
                    $('#Firstname', form).val(data.Record.Firstname);
                    $('#Lastname', form).val(data.Record.Lastname);


                    $('input[id=Country_CountryId]', form).val(data.Record.CountryId);
                    $('#FavoriteNumber', form).val(data.Record.FavoriteNumber);
                    $('#RowVersion', form).val(data.Record.RowVersion);

                    
                    $('#Country_CountryId', form).ajc().showCaption();
                    

                    setFgEditText(grid, 'Username', data.Record.Username);
                    setFgEditText(grid, 'Firstname', data.Record.Firstname);
                    setFgEditText(grid, 'Lastname', data.Record.Lastname);                    
                    setFgEditText(grid, 'FavoriteNumber', data.Record.FavoriteNumber);


                }); //showEditForm

            } //success
        }); //ajax
    }//edit

    function del(com, grid) {

        var deleteIt = confirm('Do you want to delete the selected record?');

        if (!deleteIt) return;

        var pk = getCurrentRowPk(grid);
        var version = getFgGridColumnText(grid, 'RowVersion');

        // alert(pk + " " + version + " " + encodeURIComponent(version));


        $.ajax({
            url: '/People/Management/Delete',
            type: 'POST',
            data: 'pk=' + pk + '&version=' + encodeURIComponent(version),
            success: function (result) {
                if (result.IsOk) {
                    $('#firstTable').flexReload();
                }
            }
        });

    }


</script>


Lastly we put this at the end of the html:
<script>
    $(function () {        
        _canRender = true;
    });

</script>


Here's the Flexigrid form-inliner helper functions:

function showEditForm(selectedTr, html, assignerFunc) {

    $(selectedTr).after('<tr class="fgEdit" editId=' + selectedTr.id + '><td width="1" colspan="20" style="border-width: thin; border-top: thick; border-color: #EEE; white-space: normal;"><span></span></td></tr>');
    

    var content = $('td > span', $(selectedTr).next());

    // var form = $(content).hide().html(html);
    var form = $(content).html(html);
    
    assignerFunc();
    
    $(content).show();
    
}


function showAddFormByGrid(grid, formHtml) {

    var tbl = $('.bDiv table', grid);
    showAddForm(tbl, formHtml);    
}

function showAddForm(tbl, formHtml) {
    var tbody = $('tbody', tbl);
    if (tbody.length == 0) {
        $(tbl).append($('<tbody/>'));
        tbody = $('tbody', tbl);
    }
    

    $(tbody).prepend('<tr class="fgEdit"><td width="1" colspan="20" style="border-width: thin; border-top: thick; border-color: #EEE; white-space: normal"><span></span></td></tr>');
    var content = $('tr td span', tbody);
        
    $(content).html(formHtml);


}





Flexigrid CRUD(inline form) example with ASP.NET MVC, complete with model validation:
http://code.google.com/p/flexigrid-crud-example/downloads/list


Known issues:

1. Not with flexigrid per se; but when inside of flexigrid, the jQuery Ajax ComboBox's result area is far from its textbox when using Firefox or IE, have made a work-around in Chrome though. If you could lend a help in correcting that problem of jQuery Ajax ComboBox, I'll be more than glad to accept a patch.

2. If the flexigrid has no rows, the inline form is not in full width. Accepting patch for flexigrid code or helper code.



Sample output


jQuery Ajax ComboBox's detached result screenshot(IE and Firefox problem)

$.post returns incorrect empty string

You'll receive [object XMLDocument] in your jQuery $.post's data ...

var $xhr2 = $.post( 
 options.init_src,
 {
  'lookupId': theValue,
  'field': options.field,
  'primary_key': options.primary_key,
  'db_table': options.db_table
 },
 function (data) {
  $input.val(data); // textbox receives [object XMLDocument]  
 }
);


..., when you return empty string from your controller:

public string Caption(string lookupId)
{
 return "";
}


In order to prevent that problem, just add "text" parameter to your $.post method return type


var $xhr2 = $.post( 
 options.init_src,
 {
  'lookupId': theValue,
  'field': options.field,
  'primary_key': options.primary_key,
  'db_table': options.db_table
 },
 function (data) {
  $input.val(data); // receives "" string now.  
 },
 "text"
);