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:
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.
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.
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:
Ek olarak senaryolar, “Gherkin” olarak adlandırılan linguistic bir formatta yazılıp, Given, When ve Then bölümlerinden oluşmaktadı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.
İ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:
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.
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.
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.
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<int> ProductIds { get; set; } } public interface IFavouriteService { int Create(int userId); FavouriteList GetFavouriteList(int userId, int favouriteListId); } public class FavouriteService : IFavouriteService { private readonly Dictionary<int, List<FavouriteList>> favouriteListStore = new Dictionary<int, List<FavouriteList>>(); public int Create(int userId) { int favouriteListId = new Random().Next(10); var newFavouriteList = new List<FavouriteList> { new FavouriteList { FavouriteListId = favouriteListId } }; favouriteListStore.Add(userId, newFavouriteList); return favouriteListId; } public FavouriteList GetFavouriteList(int userId, int favouriteListId) { if (favouriteListStore.TryGetValue(userId, out List<FavouriteList> 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<int> 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<int, List<FavouriteList>> favouriteListStore = new Dictionary<int, List<FavouriteList>>(); 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<FavouriteList> { new FavouriteList { FavouriteListId = favouriteListId, ProductIds = new List<int>() } }; favouriteListStore.Add(userId, newFavouriteList); return favouriteListId; } public FavouriteList GetFavouriteList(int userId, int favouriteListId) { if (favouriteListStore.TryGetValue(userId, out List<FavouriteList> 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:
Ş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.
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
https://specflow.org/getting-started/
https://specflow.org/documentation/
https://specflow.org/2018/specflow-3-public-preview-now-available/
{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…
{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…
{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…
{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…
{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…
{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…
View Comments
Thank you very much. very informative.
Thanks for your comment.
Thanks for the information,
I did add the feature file as mentioned but my feature file is not in color pattern.
I'm using MacBook Pro,
Visual Studio 2019
Spec flow, Selenium & Nunit extensions are downloaded along with the Straight8 Specflow extension and the specflow.json is added as well to the project.
Hi, maybe it can be related with versions of VS for mac or "Straight8’s SpecFlow Integration"?