İçeriğe geç

.NET Core ve Visual Studio for Mac ile Behavior Driven Development (BDD)

Agile bir development takımı düşünelim, Developer’ından Product Owner’ına, Scrum Master’ından Stake Holder’larına kadar hepsinin product development konusunda collaborative olarak birbirlerine bağlı bir şekilde çalışıyor oldukları.

Kulağa harika geliyor değil mi? Ama maalesef bu her zaman %100 mümkün olmuyor.

Peki, bugün ise Behavior Driven Development hakkında konuşacağız.

Bu makale kapsamında BDD hakkındaki bilgilerimizi tazeleyip hemen ardından macOS üzerinde .NET Core ve Visual Studio kullanarak, içerisinde bir takım basit fonksiyonlar içeren bir API’ı, BDD yaklaşımı ile geliştirmeye çalışacağız.

Açıkcası bu makaleyi tamamlayabilmek için, uzun bir süredir SpecFlow‘un, .NET Core support’unun gelmesini bekliyordum.

Bu makale kapsamında ise, aşağıdaki başlıklara sırasıyla değineceğiz:

  1. Kısaca BDD‘ı Hatırlamak
  2. BDD‘ın Faydaları
  3. macOS üzerinde .NET Core ve Visual Studio ile BDD

Kısaca BDD‘ı Hatırlamak

Temelini Test Driven Development (TDD) dan almakta olan BDD, özellikle “kaliteli kod” üretimi üzerine oldukça fazla bir şekilde yoğunlaşmaktadır.

Hemen hemen ~2 yıllık deneyimlerime dayanarak BDD‘nin bize sağladığı en büyük faydalarını söylemem gerekirse eğer, proje geliştirme aşamaları sırasında karşılaşılan iletişimden kaynaklı zorluklara karşı önemli bir aracı olması ve bize proje için harika dokümantasyon sağlıyor olmasıdır diyebilirim.

Peki, nedir bu iletişim zorlukları ve neye sebep oluyor?

Projeyi geliştirecek olan ekibin, müşterinin tüm ihtiyaçlarını doğru bir şekilde anlayabilmesi, genelde business ekiplerinin ellerindedir.

Fakat bir çok durumda business ekiplerinin teknik tarafa uzak olmalarından dolayı da, kalitesiz kod ve eksik ihtiyaçlar da ortaya çıkabilmektedir. BDD ise bu noktada “ortak bir dil” yaratarak, karşılaşılan bu durumu çözümleyebilmemize olanak sağlamaktadır. Yani, developer’lar, test ekipleri ve business ekipleri arasında iletişimin daha iyi bir hale gelebilmesi ve gereksinimlerin kolay bir şekilde anlaşılabilmesi için bir nevi rehberlik yapmaktadır.

Ayrıca BDD, business outcome‘ına doğrudan katkı sağlayacak olan behavior‘ları, developer’lara, test ekiplerine ve business ekiplerine erişilebilir bir şekilde açıkça tanımlamaktadır. BDD‘da bu tanımlamanın odak noktasında ise, user story içerisindeki gereksinimleri bulmak ve bunlara dayalı olarak acceptance test’lerini yazmak vardır. Yani projenin baştan sona acceptance criteria’ları doğrultusunda geliştirilmesi için bir yol çizmektedir.

NOT: BDD içerisinde, müşteri de development sürecinin içerisine sokulmaktadır.

Senaryolar

BDD içerisinde acceptance criteria’lar, “senaryolar” olarak tanımlanmaktadır. Senaryolar yapısaldır ve bir özelliğin farklı durumlarda veya farklı parametreler ile nasıl davranması gerektiğini açıklamaktadır.

Örneğin:

  • X kişisi Google’a girdi.
  • Arama kutusuna “kedi” yazdı.
  • Kedi ile ilgili arama sonuçları ekrana geldi.

Ek olarak senaryolar, “Gherkin” olarak adlandırılan linguistic bir formatta yazılıp, Given, When ve Then bölümlerinden oluşmaktadır.

  • Given: Senaryonun context’ini açıklamaktadır.
  • When: Action’ı tanımlamaktadır.
  • Then: Burada ise ne olacağı, yani “outcome” tanımlanmaktadır.

Gördüğümüz gibi senaryolar, basit bir kalıp ile konuşma dili olarak yazıldığı için, anlaşılması da tüm ekipler tarafından kolay bir hale gelmektedir ve ayrıca bir dokümantasyon niteliği de taşımaktadır.

Bu konu hakkındaki daha detaylı bilgiye ise, buradan ulaşabilirsiniz.

BDD‘nin Faydaları

İlk olarak BDD, test otomasyon projelerinde de kullanılan yöntemlerden birisidir. Gherkin formatında yazılan test senaryolarının “otomasyon sürecinde” kullanılmasının yanı sıra ise, projenin yaşayan ve güncel bir dokümantasyonunun da oluşmasını ayrıca sağlamaktadır.

Genel olarak faydalarına baktığımızda ise:

  • Ekibin her bir üyesi tarafından kullanılabilecek, basit ve anlaşılabilir bir dil sunmaktadır.
  • İş birliğini arttırıp, geliştirmektedir.
  • Odak noktasında müşteri vardır ve uygulamanın davranışlarını takip etmektedir.
  • Projenin güncel bir dokümantasyonunu sağlamaktadır.

Bunlarla birlikte BDD, yazılım geliştirme süreci içerisinde “end user” ve “user acceptance” testleri için harcanan zamanı da büyük ölçüde azaltmaktadır.

macOS üzerinde .NET Core ve Visual Studio ile BDD

Bir e-ticaret firmasında çalıştığımızı düşünelim. Bizden kullanıcıların beğendikleri ürünleri favori listelerine ekleyebilmeleri için bir API geliştirmemiz isteniyor. Bu API‘ı, BDD ile geliştirelim ve nasıl işleyeceğini görelim.

Ben API‘ı, macOS üzerinde Visual Studio ve .NET Core 2.2 kullanarak geliştireceğim. BDD framework’ü olarak ise, SpecFlow kullanacağız.

SpecFlow, .NET çatısı altında Gherkin parser’ını kullanarak human-readable acceptance test’leri tanımlayabilmemize ve yönetebilmemizi sağlayan, open-source bir Behavior Driven Design framework’üdür.

Öncelikle macOS üzerinde Visual Studio‘ya sahip değilseniz, buradan indirebilirsiniz. Visual Studio‘yu açtıktan sonra “Extensions” bölümüne girelim ve “Gallery” tab’ına tıklayalım. Search kutusuna “Straight8’s SpecFlow Integration” yazalım ve aşağıdaki gibi ilgili extension’ın kurulumunu gerçekleştirelim.

Bu extension sayesinde, projemize kolaylıkla feature’lar ve step definition’lar ekleyebileceğiz.

Şimdi “MyFavouriteAPI.Tests” adında bir .NET Core 2.2 NUnit Test Projesi oluşturalım. Ardından sırasıyla “SpecFlow“, “SpecFlow.Tools.MsBuild.Generation” ve “SpecFlow.NUnit” paket’lerini projeye NuGet üzerinden dahil edelim.

Genel configuration opsiyonları için ise, aşağıdaki gibi “specflow.json” adında bir configuration file’ı oluşturalım.

{
  "language": {
    "feature": "en-US"
  }
}

Bu opsiyon ile, feature file’larının İngilizce olacağını belirtmiş oluyoruz.

Configuration file’ını oluşturduktan sonra, “Features” adında bir klasör oluşturalım. Ardından bu klasör içerisine aşağıdaki gibi “FavouriteList” adında bir feature dosyası ekleyelim.

Extension, template olarak bize aşağıdaki gibi bir feature dosyası yaratacaktır.

Feature: Addition
	In order to avoid silly mistakes
	As a math idiot
	I want to be told the sum of two numbers
	
@mytag
Scenario: Add two numbers
	Given I have entered 50 into the calculator
	And I have entered 70 into the calculator
	When I press add
	Then the result should be 120 on the screen

Şimdi ise bu template’in içeriğini düzenleyerek, kendi feature senaryomuzu oluşturalım.

Senaryonun Tanımlanması

Feature olarak bizim istediğimiz şey, “kullanıcıların beğendikleri ürünleri daha sonra satın alabilmeleri için, favori listeleri oluşturup ürün ekleyebilmeleri veya silebilme işlemleri“.

Ozaman feature’ı aşağıdaki gibi düzenleyelim.

Feature: Favourite List
	A simple favourite list that we can add or remove products in order to buy them later

Şimdi ilk senaryomuzu tanımlayalım. Öncelikle bir favori listesi oluşturmaya ihtiyacımız var. Bunun için senaryo kısmına, “yeni bir favori listesi oluşturma” işlemi diyelim.

Peki bu senaryo ne zaman gerçekleşecek, yani buradaki action nedir? Buradaki action’ın tanımı için ise, “yeni bir favori listesi oluşturduğumda” desek yeterli olur sanırım. Şimdi bu işlemin sonucunda ise ne olacak, buradaki outcome nedir?

Bunun için ise, “favori listesi boş olarak oluşturulmuş olmalıdır” diyebiliriz. Bu senaryodan yola çıkarak, feature dosyasını ise aşağıdaki gibi düzenleyelim.

Feature: Favourite List
    A simple favourite list that we can add or remove products in order to buy them later
    
@mytag
Scenario: Create a new favourite list
    When I create a new favourite list
    Then the favourite list should be created as empty

Yukarıda tanımlamış olduğumuz bu senaryo, yapılacak olan işi ne kadar da net anlatıyor değil mi? Ekibin her bir üyesi tarafından kullanılabilecek ve anlaşılabilecek basit bir dil.

Senaryonun Kodlanması

Senaryoyu tanımlamanın ardından, projemizi build edelim ve IDE üzerinden “Unit Tests” pad’ine geçelim.

Ahha! Projeyi build etmenin ardından, bizim için “CreateANewFavouriteList” test’ini oluşturmuş durumda.

Ozaman test’i run edelim ve sonucuna test result pad üzerinde bir bakalım.

Henüz herhangi bir kod yazmadığımız için “No matching step definition found for one or more steps.” mesajını bize veriyor.

Şimdi ise senaryomuz ile alakalı, step definition’ları tanımlamamız gerekiyor. Bunun için ise, extension’ın result pad’de bize vermiş olduğu örnek kod parçasını kullanabiliriz.

Şimdi “StepDefinitions” adında bir klasör oluşturalım ve içerisinde, “FavouriteListSteps” adında bir class tanımlayalım.

Ardından örnek kod parçasını kopyalayalım ve aşağıdaki gibi “FavouriteListSteps” class’ı içerisine yapıştıralım.

NOT: “MyNamespace” ve “StepDefinitions” kısımlarını düzenlemeyi unutmayalım.

using System;
using TechTalk.SpecFlow;

namespace MyFavouriteAPI.Tests.StepDefinitions
{
    [Binding]
    public class FavouriteListSteps
    {
        [When(@"I create a new favourite list")]
        public void WhenICreateANewFavouriteList()
        {
            ScenarioContext.Current.Pending();
        }

        [Then(@"the favourite list should be empty")]
        public void ThenTheFavouriteListShouldBeEmpty()
        {
            ScenarioContext.Current.Pending();
        }
    }
}

Burada ne yapmamız gerektiği gayet açık, değil mi?

Kodlamaya başlamadan önce assertion’ları kolaylıkla yapabilmemiz için, projeye “FluentAssertions” paketini de NuGet üzerinden dahil edelim.

Ardından örnek senaryomuzu ise, aşağıdaki gibi kodlamaya başlayalım.

using TechTalk.SpecFlow;
using System.Collections.Generic;
using FluentAssertions;
using System;
using System.Linq;

namespace MyFavouriteAPI.Tests.StepDefinitions
{
    [Binding]
    public class FavouriteListSteps
    {
        private readonly IFavouriteService _favouriteService;
        private int _favouriteListId;
        private readonly int _userId;

        public FavouriteListSteps()
        {
            _favouriteService = new FavouriteService();
            _userId = 1;
        }

        [When(@"I create a new favourite list")]
        public void WhenICreateANewFavouriteList()
        {
            _favouriteListId = _favouriteService.Create(_userId);
        }

        [Then(@"the favourite list should be empty")]
        public void ThenTheFavouriteListShouldBeEmpty()
        {
            FavouriteList favouriteList = _favouriteService.GetFavouriteList(_userId, _favouriteListId);

            favouriteList.Should().NotBeNull();
            favouriteList.FavouriteListId.Should().Be(_favouriteListId);
            favouriteList.ProductIds.Should().BeNull();

        }
    }

    public class FavouriteList
    {
        public int FavouriteListId { get; set; }
        public List ProductIds { get; set; }
    }

    public interface IFavouriteService
    {
        int Create(int userId);
        FavouriteList GetFavouriteList(int userId, int favouriteListId);
    }

    public class FavouriteService : IFavouriteService
    {
        private readonly Dictionary> favouriteListStore = new Dictionary>();

        public int Create(int userId)
        {
            int favouriteListId = new Random().Next(10);

            var newFavouriteList = new List
            {
                new FavouriteList { FavouriteListId = favouriteListId }
            };

            favouriteListStore.Add(userId, newFavouriteList);

            return favouriteListId;
        }

        public FavouriteList GetFavouriteList(int userId, int favouriteListId)
        {
            if (favouriteListStore.TryGetValue(userId, out List userFavouriteList))
            {
                var favouriteList = userFavouriteList.FirstOrDefault(_ => _.FavouriteListId == favouriteListId);

                return favouriteList;
            }

            return null;
        }
    }
}

Burada basitçe “When” ve “Then” adımlarımızı kodladık. İlk önce kullanıcı için yeni bir favori listesi yarattık, ardından favori listesinin boş olduğunu doğruladık.

Şimdi “CreateANewFavouriteList” test’ini tekrar çalıştıralım.

Gördüğümüz gibi test başarıyla geçti.

Şimdi feature’a yeni bir senaryo daha ekleyelim. Kullanıcı artık favori listesine, ürün ekleyebilmeli. Birde favori listesinden ürünü silebilmeli.

Bunun için senaryomuzu aşağıdaki gibi genişletelim.

Feature: Favourite List
    A simple favourite list that we can add or remove products in order to buy them later
    
@mytag
Scenario: Create a new favourite list
    When I create a new favourite list
    Then the favourite list should be empty
    
Scenario: Add a new product to the favourite list
    Given I create a new favourite list
    When I select the favourite list and press the add favourite button on the product detail page
    Then the product should be added to the favourite list

Scenario: Remove a product from the favourite list
    Given I create a new favourite list
    And I select the favourite list and press the add favourite button on the product detail page
    When I press the remove product button on the favourite list page
    Then the product should be removed from the favourite list

Senaryoyu kaydettikten sonra projeyi tekrar build edelim ve “Unit Tests” pad’ine tekrar bir bakalım.

Build işleminin ardından, yeni tanımlamış olduğumuz senaryo için de extension, “AddANewProductToTheFavouriteList” ve “RemoveAProductFromTheFavouriteList”  isimli test’ler oluşturulmuş durumda.

Test’leri run edelim ve yine test result pad ekranındaki örnek method snippet’larını alalım.

No matching step definition found for one or more steps.
using System;
using TechTalk.SpecFlow;

namespace MyNamespace
{
    [Binding]
    public class StepDefinitions
    {
        [Given(@"I create a new favourite list")]
        public void GivenICreateANewFavouriteList()
        {
            ScenarioContext.Current.Pending();
        }
        
        [When(@"I select the favourite list and press the add favourite button on the product detail page")]
        public void WhenISelectTheFavouriteListAndPressTheAddFavouriteButtonOnTheProductDetailPage()
        {
            ScenarioContext.Current.Pending();
        }
        
        [Then(@"the product should be added to the favourite list")]
        public void ThenTheProductShouldBeAddedToTheFavouriteList()
        {
            ScenarioContext.Current.Pending();
        }
    }
}

No matching step definition found for one or more steps.
using System;
using TechTalk.SpecFlow;

namespace MyNamespace
{
    [Binding]
    public class StepDefinitions
    {
        [Given(@"I create a new favourite list")]
        public void GivenICreateANewFavouriteList()
        {
            ScenarioContext.Current.Pending();
        }
        
        [Given(@"I select the favourite list and press the add favourite button on the product detail page")]
        public void GivenISelectTheFavouriteListAndPressTheAddFavouriteButtonOnTheProductDetailPage()
        {
            ScenarioContext.Current.Pending();
        }
        
        [When(@"I press the remove product button on the favourite list page")]
        public void WhenIPressTheRemoveProductButtonOnTheFavouriteListPage()
        {
            ScenarioContext.Current.Pending();
        }
        
        [Then(@"the product should be removed from the favourite list")]
        public void ThenTheProductShouldBeRemovedFromTheFavouriteList()
        {
            ScenarioContext.Current.Pending();
        }
    }
}

Şimdi feature’ı tamamlayabilmek için bizden beklemiş olduğu bu davranışları, “FavouriteListSteps” class’ı içerisinde aşağıdaki gibi implemente edelim.

using TechTalk.SpecFlow;
using System.Collections.Generic;
using FluentAssertions;
using System;
using System.Linq;
namespace MyFavouriteAPI.Tests.StepDefinitions
{
[Binding]
public class FavouriteListSteps
{
private readonly IFavouriteService _favouriteService;
private int _favouriteListId;
private readonly int _userId;
private readonly int _productId;
public FavouriteListSteps()
{
_favouriteService = new FavouriteService();
_userId = 1;
_productId = 1;
}
[Given(@"I create a new favourite list")]
[When(@"I create a new favourite list")]
public void WhenICreateANewFavouriteList()
{
_favouriteListId = _favouriteService.Create(_userId);
}
[Then(@"the favourite list should be empty")]
public void ThenTheFavouriteListShouldBeEmpty()
{
FavouriteList favouriteList = _favouriteService.GetFavouriteList(_userId, _favouriteListId);
favouriteList.Should().NotBeNull();
favouriteList.FavouriteListId.Should().Be(_favouriteListId);
favouriteList.ProductIds.Should().BeEmpty();
}
[Given(@"I select the favourite list and press the add favourite button on the product detail page")]
[When(@"I select the favourite list and press the add favourite button on the product detail page")]
public void WhenISelectTheFavouriteListAndPressTheAddFavouriteButtonOnTheProductDetailPage()
{
_favouriteService.AddFavourite(_userId, _favouriteListId, _productId);
}
[Then(@"the product should be added to the favourite list")]
public void ThenTheProductShouldBeAddedToTheFavouriteList()
{
FavouriteList favouriteList = _favouriteService.GetFavouriteList(_userId, _favouriteListId);
favouriteList.Should().NotBeNull();
favouriteList.FavouriteListId.Should().Be(_favouriteListId);
favouriteList.ProductIds.Should().Contain(_productId);
}
[When(@"I press the remove product button on the favourite list page")]
public void WhenIPressTheRemoveProductButtonOnTheFavouriteListPage()
{
_favouriteService.RemoveProduct(_userId, _favouriteListId, _productId);
}
[Then(@"the product should be removed from the favourite list")]
public void ThenTheProductShouldBeRemovedFromTheFavouriteList()
{
FavouriteList favouriteList = _favouriteService.GetFavouriteList(_userId, _favouriteListId);
favouriteList.Should().NotBeNull();
favouriteList.FavouriteListId.Should().Be(_favouriteListId);
favouriteList.ProductIds.Should().NotContain(_productId);
}
}
public class FavouriteList
{
public int FavouriteListId { get; set; }
public List ProductIds { get; set; }
}
public interface IFavouriteService
{
void AddFavourite(int userId, int favouriteListId, int productId);
int Create(int userId);
FavouriteList GetFavouriteList(int userId, int favouriteListId);
void RemoveProduct(int userId, int favouriteListId, int productId);
}
public class FavouriteService : IFavouriteService
{
private readonly Dictionary> favouriteListStore = new Dictionary>();
public void AddFavourite(int userId, int favouriteListId, int productId)
{
FavouriteList favouriteList = GetFavouriteList(userId, favouriteListId);
if(favouriteList != null)
{
favouriteList.ProductIds.Add(productId);
}
}
public int Create(int userId)
{
int favouriteListId = new Random().Next(10);
var newFavouriteList = new List
{
new FavouriteList 
{ 
FavouriteListId = favouriteListId,
ProductIds = new List() 
}
};
favouriteListStore.Add(userId, newFavouriteList);
return favouriteListId;
}
public FavouriteList GetFavouriteList(int userId, int favouriteListId)
{
if (favouriteListStore.TryGetValue(userId, out List userFavouriteList))
{
var favouriteList = userFavouriteList.FirstOrDefault(_ => _.FavouriteListId == favouriteListId);
return favouriteList;
}
return null;
}
public void RemoveProduct(int userId, int favouriteListId, int productId)
{
FavouriteList favouriteList = GetFavouriteList(userId, favouriteListId);
if (favouriteList != null)
{
favouriteList.ProductIds.Remove(productId);
}
}
}
}

Burada bir kaç noktaya değinmek istiyorum. Eğer daha önce implemente ettiğimiz benzer bir senaryo varsa, onu tekrardan kodlamamıza gerek yok. Tek yapmamız gereken, feature file’ında da olduğu gibi, “Given” context’ini gerekli yere eklemek.

Örneğin favori listesine yeni bir ürün ekleyebilmek ve silebilmek için, önce bir favori listesi oluşturmamız gerekmektedir. Bunun için ise daha önce implemente etmiş olduğuz “WhenICreateANewFavouriteList” method’una, “[Given(@”I create a new favourite list”)]” attribute’ünü eklememiz yeterli olacaktır.

Devamında ise, bizden beklenen davranışları implemente ettik. Şimdi “Unit Tests” pad’ine tekrar geçelim ve tüm test’leri çalıştıralım.

Tada! “FavouriteList” feature’ının tamamlanabilmesi için gerekli tüm senaryolar başarıyla geçti.

Makalenin giriş kısmında BDD‘nin faydalarından bahsederken, aşağıdaki madde’lerden bahsetmiştik:

  • Ekibin her bir üyesi tarafından kullanılabilecek, basit ve anlaşılabilir bir dil sunmaktadır.
  • İş birliğini arttırıp, geliştirmektedir.
  • Odak noktasında müşteri vardır ve uygulamanın davranışlarını takip etmektedir.
  • Projenin güncel bir dokümantasyonunu sağlamaktadır.

Şimdi ise oluşturmuş olduğumuz feature file’ına bir bakalım.

Feature: Favourite List
A simple favourite list that we can add or remove products in order to buy them later
@mytag
Scenario: Create a new favourite list
When I create a new favourite list
Then the favourite list should be empty
Scenario: Add a new product to the favourite list
Given I create a new favourite list
When I select the favourite list and press the add favourite button on the product detail page
Then the product should be added to the favourite list
Scenario: Remove a product from the favourite list
Given I create a new favourite list
And I select the favourite list and press the add favourite button on the product detail page
When I press the remove product button on the favourite list page
Then the product should be removed from the favourite list

Bu feature file, ekibin her bir üyesi tarafından kullanılabilecek basit ve anlaşılabilir bir dile sahip, projenin güncel bir dokümantasyonudur. Development sırasında ise bizi, uygulamanın davranışlarını takip ettirerek, kodumuza yön vermiştir.

Sonuç

BDD, özellikle product development konusunda collaborative olarak işbirliğine ihtiyaç duyulduğu zamanlarda kullanılabilecek önemli bir methodology’dir. Kullanıcıyı ve uygulamanın davranışlarını da odağına alarak, maintanance ve ek maliyetleri de minimize etmektedir. Ayrıca uygulamanın güncel dokümantasyonunu da oluşturmasıyla birlikte, test otomasyon sürecine de büyük ölçüde destek vermektedir.

Link: https://github.com/GokGokalp/BDDSampleWithNetCoreSpecFlow

Referanslar

https://specflow.org/getting-started/
https://specflow.org/documentation/
https://specflow.org/2018/specflow-3-public-preview-now-available/

Bu makale toplam (1268) kez okunmuştur.

Tarih:.NET CoreASP.NET CoreBehavior Driven DevelopmentTest Driven Development

İlk Yorumu Siz Yapın

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

This site uses Akismet to reduce spam. Learn how your comment data is processed.