The Power of Refactoring

Project yang sedang dikerjakan sekarang adalah membuat Mobil App untuk membaca score (notasi musik) dengan beberapa fitur seperti pengaturan tempo, looping, environment, change instrument, adding annotation dan sebagainya. Masalahnya kemudian,hampir semua developer yang terlibat adalah backend programmer yang tidak punya pengalaman mengerjakan mobile app dan juga tidak familiar menggunakan pattern MVVM. Ya, projectnya menggunakan Xamarin dan MvvmCross framewrok.

Nah, pada sprint kesekian project pengerjaan projectnya semakin melambat. Salah satu penyebabnya adalah beberapa task mengharuskan menunggu pengerjaan developer yang lain karena mengharuskan bekerja pada class/module yang sama. Begitu juga bagian FrontEnd mengaku kesulitan dalam mengintegrasikan designnya karena harus bersentuhan dengan code program yang berhubungan dengan funcionality.

Akhirnya diputuskan dalam satu sprint di adakan refactor besar-besaran, tidak ada penambahan fitur. Refacor focus pada aspek Single Responsibility dan Separation of Concern. Juga melengkapi unit testing sehingga dapat dengan mudah dideteksi mana code yang termasuk smell code. Ya, salah satu tandanya adalah jika code tersebut tidak testable. Ternyata setelah mengadakan refactor yang cukup signifikan, Sprint berikutnya pekerjaan menjadi lebih mudah dan cepat.

Error When Call IMvxCommand in Unit Testing

Kode unit testing di bawah ini ketika memanggil SampleClickCommand menyebabkan error Null Object Reference.

[TestMethod]
       public void WhenPlayClick_SampleMusicIsPlaying()
       {
           _mockAudioService.Setup(arg => arg.PlayMusic(It.IsAny<string>()));
           _mockAudioService.Setup(arg => arg.IsMusicPlaying).Returns(true);
           _scoreLibraryCellViewModel = new ScoreLibraryCellViewModel(_mockAudioService.Object, _mockFileAccessService.Object, _mockDialogService.Object);
           _scoreLibraryCellViewModel.SampleClickCommand.Execute();
 
 
 
       }

Untuk meresolve error ini register type untuk IMvxStringToTypeParser saat initialisasi.

var ioc = MvxSimpleIoCContainer.Initialize();
          ioc.RegisterSingleton<IMvxStringToTypeParser>(new MvxStringToTypeParser());

 

How to update ObservableCollection in MvvmCross when property of Item is changes

Ketika develop android app menggunakan Xamarin. Saya menggunakan MvvmCross sebagai framework. Dalam MvvM patterna kita akan selalu berurusan dengan Binding data antara View(UI)- dan ViewModel.

Saat saya melakukan binding data ObservableColletion pada ViewModel dengan ListView pada View ternyata event Collection Changes pada ObservableCollection tidak di rise ketika ada perubahan pada property di itemnya. Jadi setelah saya baca-baca, event Collection Changes ini hanya di rise ketika ada penambahan atau pengurangan jumlah item pada ObservableCollection.

Layout View.

 
 

ViewModel:

ProductListItemViewModel:

public class ProductListItemViewModel : MvxViewModel
   {
       
       public int Id { get; set; }
 
       public string Name { get; set; }
 
       private decimal _quantity;
 
       
       public decimal Quantity
       {
           get
           {
               return _quantity;
 
           }
           set
           {
               _quantity = value;
               RaisePropertyChanged(() => Quantity);
               
           }
       }

ProductListViewModel.

private ObservableCollection _displayedProducts;
       public ObservableCollection DisplayedProducts
       {
           get { return _displayedProducts; }
           set
           {
               _displayedProducts = value;
               RaisePropertyChanged(() => DisplayedProducts);
               Calculate();
           }
       }
       private void Calculate()
       {
           decimal totalSales = 0;
           decimal totalGst = 0;
           foreach (var product in DisplayedProducts)
           {
               var sales = (product.Price * product.Quantity);
               totalSales += sales;
               if (product.GstFlag)
               {
                   totalGst += sales * Utility.Constants.GST_RATE;
               }
           }
           TotalSales = totalSales;
           TotalGst = totalGst;
       }
       private decimal _totalSales;
       public Decimal TotalSales
       {
           get
           {
               
               return _totalSales;
           }
           set
           {
               _totalSales = value;
               RaisePropertyChanged(() => TotalSales);
           }
           
       }
       private decimal _totalGst;
       public Decimal TotalGst
       {
           get
           {
 
               return _totalGst;
           }
           set
           {
               _totalGst = value;
               RaisePropertyChanged(() => TotalGst);
           }
 
       }

Karena saya ingin ketika ada perubahan quantity pada Product (inputnya ditrigger dari editable text field pada listview), calculate() method dipanggil lagi sehingga property TotalSales dan TotalGst akan diupdate otomatis.

Saya menemukan solusinya dengan menset event PropertyChanges pada ProductListItemViewModel sebelum menambahkannya pada ObservableCollection seperti ini.

productViewItemModel.PropertyChanged += ProductViewItemModel_PropertyChanged;
               productViewModels.Add(productViewItemModel);
private void ProductViewItemModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            Calculate();
        }

code lengkapnya ada di https://github.com/adnansetiawan/sales-app-xamarin

Verb DELETE Return 405

Hari ini teman saya ada masalah, verb ‘Delete’ pada Web Api mengembalikan 405 pada server Dev. Akhirnya kami menemukan 2 solusi.

  1.  IIS-> click {nama_website} -> click Handler Mappings -> pilih WebDAV kemudian click button Request Restrictions. Pilih All Verbs

web_dav

  1. Pada WebConfig tambahkan handler pada <system.webServer>.
<modules runAllManagedModulesForAllRequests="true">
    <remove name="WebDAVModule"/> <!-- add this -->
</modules>
 dan pada handle section, tambahkan
<handlers>
    <remove name="WebDAV" />
    ...
</handlers>

Basic Regular Expression

Regular Expression wajib diketahui oleh seorang programmer karena seringkali digunakan untuk mencari atau memvalidasi sebuah string. Regular Expression hampir bisa digunakan di semua bahasa pemrograman. Khusus untuk C#, contoh penggunaannya seperti ini:

Regex regex = new Regex(@"\d+ files? found\?");
var word = "1 file found?";
Match match = regex.Match(word);
Console.WriteLine($"{word} is match ? {match.Success}");

Beberapa basic ekspresi Regular Expression yang paling sering digunakan yang saya rangkum dari  Regexone

Semua string yang mengandung “abc”

abc

Task Text
Match abcd123
Match 123abc4
Skip abd
Skip bcd

Semua string yang didahului oleh karakter”c” atau “m” dan diakhir oleh “an”.

[cm]an 

Semua string yang didahului oleh selain karakter “f” dan diakhir oleh “an”.

[^f]an

Task Text
Match can
Match man
Skip fan

Semua string yang di awali -case sensitif dengan karakter pertama adalah “A” sampai “C”, karater kedua adalah “n” sampai “p” dan karakter ketiga adalah “a” sampai “c”

[A-C][n-p][a-c]

Task Text
Match Ana
Match Bob
Match Cpc
Skip aax
Skip bby
Skip ccz

Semua string yang diawali dengan “wa” dan setelah itu memiliki minimal 3 dan maksimal  4 karater “z” dan diakhiri oleh “up”.

waz{3,4}up

Task Text
Skip wazzzzzup
Match wazzzzup
Match wazzzup
Skip wazup

Semua string yang diawali oleh minimal 2 dan maksimal 4 karakter “a” setelah itu maksimum 4 karakter “b” (karakter b bisa tidak ada) dan minimum 1 dan maksimum 2 karakter “c”.

a{2,4}b{0,4}c{1,2}

Task Text
Match aaaabcc
Match aabbbbc
Match aacc
Skip a
Skip ab
Skip bc

Semua string yang didahului oleh karakter angka, kemudian “file” atau “files” disusul “found?”. Metakarakter “?” menunjukkan optional dan metakarakter “\d” mewakili angka.

\d+ files? found\?

Task Text
Match 1 file found?
Skip 1 file found
Match 2 files found?
Skip No files found

Semua string yang di dahului oleh angka kemudian titik setelah itu whitespace diikuti “abc”. White space diwakili oleh metakarakter “\s”

\d.\s+abc

Task Text
Match 1.       abc
Match 2.           abc
Match 3.               abc
Skip 4.abc

Semua string yang mengandung persis keseluruhan “Mission: successful”. Metakarakter “^” artinya didahului oleh dan “$” artinya diakhiri oleh.

^Mission: successful$ 

Task Text
Match Mission: successful
Skip Last Mission: unsuccessful
Skip Next Mission: successful upon capture of target

Mencapture string yang diawali dengan “file” dan diakhiri dengan “.pdf”. Metakarakter () mencapture group karakter yang diapit olehnya.

^(file.+).pdf$

Task Text Capture Group
Capture file_record_transcript.pdf file_record_transcript
Capture file_07241999.pdf file_07241999
Skip testfile_fake.pdf.tmp

 

 

Menangani Error: The provided anti-forgery token was meant for a different claims-based user than the current user.

Hari ini saya mendapatkan ticket dari Tester mengenai error authentikasi user. Test case nya seperti ini.

  1. Navigasi ke halaman Login di tab pertama, login sebagai user ‘A’.
  2. Kemudian buka tab baru pada browser, buka home page (selain halaman login).
  3. Logout user di tab pertama.
  4. Buka halaman lain pada tab 2, page akan redirect ke halaman login dengan membawa serta query string return url.
  5. Login di tab pertama.
  6. ketika login di tab kedua akan muncul error “The provided anti-forgery token was meant for a different claims-based user than the current user”.

Pertama saya mencoba dengan menonaktifkan cache pada action login  dengan menambahkan attribut [OutputCache(NoStore = true, Location = OutputCacheLocation.None)] tapi ternyata hasil nya tetap sama.

Cara kedua, yaitu dengan cara menghilangkan [ValidateAntiForgeryToken] pada action login, tapi tetap menambahkan  @Html.AntiForgeryToken() pada form. Tapi cara ini malah mengurangi tingkat keamanan pada website.

Akhirnya, setelah googiling. Saya menemukan solusi yang lebih tepat dengan cara membuat attribute untuk menangani error tersebut. Sehingga jika terjadi error tersebut bisa dialihkan ke halaman lain.

public class AntiForgeryHandleErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext context)
{
if (context.Exception is HttpAntiForgeryException)
{
var url = string.Empty;
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
var requestContext = new RequestContext(context.HttpContext, context.RouteData);
url = RouteTable.Routes.GetVirtualPath(requestContext, new RouteValueDictionary(new { Controller = "User", action = "Login" })).VirtualPath;
}
else
{
context.HttpContext.Response.StatusCode = 200;
context.ExceptionHandled = true;
url = GetRedirectUrl(context);
}
context.HttpContext.Response.Redirect(url, true);
}
else
{
base.OnException(context);
}
}

private string GetRedirectUrl(ExceptionContext context)
{
try
{
var requestContext = new RequestContext(context.HttpContext, context.RouteData);
var url = RouteTable.Routes.GetVirtualPath(requestContext, new RouteValueDictionary(new { Controller = "Home", action = "Index" })).VirtualPath;

return url;
}
catch (Exception)
{
throw new NullReferenceException();
}
}

Tambahkan attribute tersebut pada login action

[HttpPost]
       [AllowAnonymous]
       [ValidateAntiForgeryToken]
       [AntiForgeryHandleError]
       public async Task Login(LoginViewModel model, string returnUrl)
       {

Behavior Driven Development (BDD) Using SpecFlow

Behavior Driven Development (or BDD) is an Agile software development technique that encourages collaboration between developers, QA, and non-technical or business participants in a software project. 

Dalam BDD kita menggunakan aspek Given/When/Then ketika membuat Feature. Dari Feature ini,  Tester dapat membuat scenario test. Sedangkan  bagi Developer bisa digunakan untuk membuat Unit Testing. Hindari menggunakan terlalu banyak istilah teknis di dalam feature yang kita buat. Salah satu tujuan menggunakan BDD adalah agar menghindari salah pengertian mengenai sebuah feature antara tester, analis dan developer. Kita akan menggunakan SpecFlow sebagai framework BDD kita.

Sebagai contoh kita mempunyai story seperti ini. Story ini dibuat menggunakan SpecFlow feature file.

Feature: Add Vehicle
    As an Administrator
    I want to be to add new vehicle
    In order to a Customer can reserve it

@mytag
Scenario: Vehicle Name is not blank
    Given I have entered '' into the field name
    When I press Submit
    Then I see message saying name is mandatory

Kemudian klik kanan file feature yang kita buat untuk membuat step.

right_click_generate_steps

steps_bdd.png

SpecFlow akan menghasilkan file steps seperti ini.

using System;
using TechTalk.SpecFlow;

namespace BDDTest
{
[Binding]
public class AddVehicleSteps
{
[Given(@"I have entered '(.*)' into the field name")]
public void GivenIHaveEnteredIntoTheFieldName(string p0)
{
ScenarioContext.Current.Pending();
}

[When(@"I press Submit")]
public void WhenIPressSubmit()
{
ScenarioContext.Current.Pending();
}

[Then(@"I see message saying name is mandatory")]
public void ThenISeeMessageSayingNameIsMandatory()
{
ScenarioContext.Current.Pending();
}
}
}

Kita running unit step tersebut, terlihat bahwa unit tersebut diskip karena belum ada implementasi di dalamnya.

skipped_test.png

Kita akan membuat class untuk menghandle bisnis logic untuk feature tersebut dan memodifikasi class steps di atas.

using BDDProject;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using TechTalk.SpecFlow;

namespace BDDTest
{
[Binding]
public class AddVehicleSteps
{
private IVehicleBLL _vechileBll = new VehicleBLL();
private Vehicle _vehicle = new Vehicle();
private string _message;
[Given(@"I have entered '(.*)' into the field name")]
public void GivenIHaveEnteredIntoTheFieldName(string EmptyName)
{
_vehicle.Name = EmptyName;
}

[When(@"I press Submit")]
public void WhenIPressSubmit()
{
try
{
_vechileBll.Add(_vehicle);
}
catch (Exception ex)
{
_message = ex.Message;
}
}

[Then(@"I see message saying name is mandatory")]
public void ThenISeeMessageSayingNameIsMandatory()
{
Assert.AreEqual(_message.ToLower(), "name is mandatory");
}
}
}

Kita jalankan lagi unit test kita, dan hasilnya seperti ini.

bdd_output.png

Refactoring Using CodeMaid

CodeMaid adalah ekstensi open source untuk visual studio yang bisa digunakan untuk melihat tingkat kompleksitas code program kita selain melakukan formating dan mengorganisir kode. Semakin tinggi nilai kompleksitas sebuah fungsi, semakin butuh fungsi itu untuk direfactor. Karena salah satu prinsip clean code adalah satu method harusnya hanya memiliki 1 high level abstraksi.

contoh code dengan kompleksitas yang tinggi seperti di bawah ini.

private Expression<Func<Vehicle, bool>> CreateVehicleFilter(VehicleFilterInput input)
{
Expression<Func<Vehicle, bool>> queryFilter = PredicateHelper.True();

if (!string.IsNullOrEmpty(input.Name))
queryFilter = queryFilter.And(ve => ve.VehicleTranslations.Any(t => t.Language == input.Language) ?
ve.VehicleTranslations.Any(t => t.Language == input.Language && t.TranslatedText.ToLower().Contains(input.Name.ToLower()))
: ve.Name.ToLower().Contains(input.Name.ToLower()));

if (!string.IsNullOrEmpty(input.InternalNumber))
queryFilter = queryFilter.And(ve => ve.InternalNumber.ToLower().Contains(input.InternalNumber.ToLower()));

if (!string.IsNullOrEmpty(input.ChassisNumber))
queryFilter = queryFilter.And(ve => ve.Chassis.ToLower().Contains(input.ChassisNumber.ToLower()));

if (input.VehicleBrandId != null && input.VehicleBrandId != 0)
queryFilter = queryFilter.And(ve => ve.VehicleModel.VehicleBrand.Id == input.VehicleBrandId);

if (input.VehicleModelId != null && input.VehicleModelId != 0)
queryFilter = queryFilter.And(ve => ve.VehicleModel.Id == input.VehicleModelId);

if (input.DateOfEntry != null && input.DateOfEntry != DateTime.MinValue &&
input.DateOfEntryTo != null && input.DateOfEntryTo != DateTime.MinValue)
queryFilter = queryFilter.And(ve => ve.DateEntry >= input.DateOfEntry && ve.DateEntry <= input.DateOfEntryTo); else { if (input.DateOfEntry != null && input.DateOfEntry != DateTime.MinValue) queryFilter = queryFilter.And(ve => ve.DateEntry >= input.DateOfEntry);
if (input.DateOfEntryTo != null && input.DateOfEntryTo != DateTime.MinValue)
queryFilter = queryFilter.And(ve => ve.DateEntry <= input.DateOfEntryTo); } if (input.ReleaseDate != null && input.ReleaseDate != DateTime.MinValue && input.ReleaseDateTo != null && input.ReleaseDateTo != DateTime.MinValue) { queryFilter = queryFilter.And(ve => (ve.DateActualRelease >= input.ReleaseDate && ve.DateActualRelease <= input.ReleaseDateTo) || (ve.DateSupposedRelease >= input.ReleaseDate && ve.DateSupposedRelease <= input.ReleaseDateTo)); } else { if (input.ReleaseDate != null && input.ReleaseDate != DateTime.MinValue) { queryFilter = queryFilter.And(ve => (ve.DateActualRelease >= input.ReleaseDate) || (ve.DateSupposedRelease >= input.ReleaseDate));
}
if (input.ReleaseDateTo != null && input.ReleaseDateTo != DateTime.MinValue)
{
queryFilter = queryFilter.And(ve => (ve.DateActualRelease <= input.ReleaseDateTo) || (ve.DateSupposedRelease <= input.ReleaseDateTo)); } } if (!string.IsNullOrEmpty(input.Status)) queryFilter = queryFilter.And(ve => ve.Status.ToLower().Contains(input.Status.ToLower()));

return queryFilter;
}

Terlihat pada CodeMaid Spade di Visual Studio fungsi tersebut kompleksitasnya sangat tinggi, di atas nilai 10.

code_main_high_complexity

Kita refactor, dengan membaginya ke dalam beberapa fungsi yang lebih kecil seperti ini.

private Expression<Func<Vehicle, bool>> CreateVehicleFilter(VehicleFilterInput input)
{
Expression<Func<Vehicle, bool>> queryFilter = PredicateHelper.True();

FilterVehicleByName(input, ref queryFilter);
FilterByDateOfEntry(input, ref queryFilter);
FilterByChassisNumber(input, ref queryFilter);
FilterByInternalNumber(input, ref queryFilter);
FilterByVehicleBrand(input, ref queryFilter);
FilterByVehicleModel(input, ref queryFilter);
FilterByRangeReleaseDate(input, ref queryFilter);
FilterByStatus(input, ref queryFilter);
return queryFilter;

}

CodeMaid akan menunjukan bahwa tingkat kompleksitasnya sekarang jauh lebih sedikit.

code_main_low_complexity

Error dialog when Open .cshtml File

Saya mendapat error pagi ini. Ketika melakukan double click pada View (.cshtml) file di Asp.Net MVC project pada Visual Studio, muncul popup dialog dengan pesan error. Setelah googling, ternyata untuk mensolve issue ini caranya sangat mudah.

  1. Tutup Visual Studio
  2. Hapus semua file cache pada folder %LocalAppData%\Microsoft\VisualStudio\14.0\ComponentModelCache
  3. Buka kembali Visual Studio. Harusnya tidak ada lagi error seperti sebelumnya.

Mocking Database Context In Repository Unit Test Using Effort

Seringkali kita ingin membuat unit test di level repository untuk menguji query yang kita buat pada sebuah repository. Karena pada unit testing, prinsipnya adalah melakukan isolasi dari external class ataupun external resource dan eksekusi unit testing harus bisa secepat mungkin, maka kita hindari menggunakan database secara langsung.

Jika menggunakan database secara langsung, sangat berpengaruh pada kecepatan eksekusi unit testing dan masalah data yang harus diinsert dan dihapus ketika selesai digunakan oleh unit testing. Salah satu solusinya adalah menggunakan in-memory-database. In-memory database ini akan dihancurkan oleh Garbage collector ketika sudah tidak digunakan.

Untuk mempermudah kita bisa menggunakan Effort. Instalasi cukup mudah, cukup di install lewat nuget package manager.

Pada initialisasi unit test kita mockup context kita.

Pertama, buat connection untuk Effort.

var connection = DbConnectionFactory.CreateTransient();

Selanjutnya pada class context, kita tambahkan satu constuctor untuk melewatkan connection dari Effort tersebut.

public BookStoreEntities(DbConnection connection) : base(connection, true)
{
}

pada unit test initialization method lewatkan connection Effort tadi ke context dan tambahkan code untuk membuat database jika tidak ada. Ini untuk menghindari munculnya error migrations data nantinya.

_context = new BookStoreEntities(connection);
_context.Database.CreateIfNotExists();

selanjutnya, insert data untuk table table yang akan kita gunakan nantinya.

_context.Categories.Add(new Category { Name = "Programming" });
_context.SaveChanges();
var category = _context.Categories.First();
_context.Books.Add(new Book { Title="Effort In Action", Category = category });
_context.Books.Add(new Book { Title = "C# In Action", Category = category });
_context.SaveChanges();

Kode program selengkapnya seperti ini.

[TestClass]
public class BookRepositoryTest
{
private BookStoreEntities _context;
private IUnitOfWork _unitOfWork;
private IGenericRepository _bookRepository;
[TestInitialize]
public void Setup()
{

var connection = DbConnectionFactory.CreateTransient();
_context = new BookStoreEntities(connection);
_context.Database.CreateIfNotExists();

_context.Categories.Add(new Category { Name = "Programming" });
_context.SaveChanges();
var category = _context.Categories.First();
_context.Books.Add(new Book { Title="Effort In Action", Category = category });
_context.SaveChanges();
_unitOfWork = new EFUnitOfWork(_context);
_bookRepository = _unitOfWork.GetGenericRepository();
}
[TestCleanup]
public void Cleanup()
{
_context = null;
_unitOfWork = null;
_bookRepository = null;
}
[TestMethod]
public void GetAll()
{
var books = _bookRepository.Get().ToList();
Assert.AreNotEqual(0, books.Count);
}
}

[TestMethod]
public void FindByName()
{
var books = _bookRepository.Get(x=>x.Title.Contains("Effor")).ToList();
Assert.AreNotEqual(0, books.Count);
}
[TestMethod]
public void GetById()
{
var book = _bookRepository.GetById(1);
Assert.IsNotNull(book);
}

Jika unit Test kita jalankan hasilnya seperti ini.

unit-test-effort

 

Selamat mencoba!