.Net core + NUnit testujeme statickou třídu

Testování statických tříd je .Net velice jednoduché. Nemusíte nic mockovat a jednoduše napíšete test. NUnit umožňuje testovat statické třídy stejně jako běžné třídy, ale nemáte k dispozici instanci třídy, takže nemůžete testovat instanční metody ani vlastnosti. Místo toho se zaměříme na testování statických metod a vlastností.

public static class MathUtility
{
    public static int Add(int a, int b)
    {
        return a + b;
    }

    public static int Subtract(int a, int b)
    {
        return a - b;
    }
}

Ve Visual Studiu založíme ve stejné solution nový projekt, který bude vycházet ze šablony Nunit Test Project. Do toho projektu založíme nový test:

namespace Test.Statics
{
    [TestFixture]
    public class MathUtilityTests
    {
        [Test]
        public void Add_ShouldReturnCorrectSum()
        {
            int result = MathUtility.Add(3, 5);
            Assert.AreEqual(8, result);
        }

        [Test]
        public void Subtract_ShouldReturnCorrectDifference()
        {
            int result = MathUtility.Subtract(10, 3);
            Assert.AreEqual(7, result);
        }
    }
}

Po spuštění testu (na testovacím projektu klikneme pravým talčítkem a vyberme volbu Run tests) vidíme výsledky testů

Jak funguje Assert.AreEqual

Metoda Assert.AreEqual(8, result) v NUnit je způsob, jak otestovat, zda očekávaná hodnota (v tomto případě 8) je rovna aktuální hodnotě (v tomto případě result). Pokud jsou tyto dvě hodnoty shodné, test projde a pokračuje v běhu testů. Pokud jsou hodnoty různé, test selže a testovací běh je přerušen.

Zde je, jak to funguje:

  • Pokud result (aktuální hodnota) a 8 (očekávaná hodnota) jsou stejné, test projde a není vyvolána výjimka.
  • Pokud result a 8 nejsou stejné, test selže a vyvolá se výjimka AssertionException, která způsobí, že testovací běh se zastaví a označí test jako nevyhovující.

Tato metoda je používána k porovnání dvou hodnot a určení, zda jsou shodné. Použití této metody v testech je běžným způsobem ověření správného chování kódu. V tomto konkrétním případě se testuje, zda výstup z metody MathUtility.Add(3, 5) je roven 8.

Poznámka: Můžete také použít jiné metody Assert, jako například Assert.IsTrue, Assert.IsFalse, nebo Assert.IsNull, atd., v závislosti na tom, co chcete otestovat.

Vylepšení

Můžete si všimnout, že Visual Studio řádek s ověřením výsledku podtrhne, protože se mu na něm něco nelíbí:

Assert.AreEqual(8, result);

U řádku vidíme varování: Consider using the constraint model, Assert.That(actual, Is.EqualTo(expected)), instead of the classic model, Assert.AreEqual(expected, actual).

Více o problému se dočete zde: https://docs.nunit.org/articles/nunit-analyzers/NUnit2005.html

Pokud se chcete toto varování opravit, přepište ověření výsledku na:

Assert.That(result, Is.EqualTo(7));

.net Core API – nahrání souborů a json objektů

Dnes se podíváme na problém, se kterým se můžete setkat v .net core API.

Zadání: založte POST endpoint, který přijme pole souborů a pole objektů (informace pro každý soubor).

Zní to jednoduše, však řešení má trochu háček. pojďme si to ukázat. Založme request:

using System.ComponentModel.DataAnnotations;

namespace ApiExample.Dtos
{
    public class SnapRequest
    {
        /// <summary>
        /// Metadata collection
        /// </summary>
        [Required]
        public required IEnumerable<Metadata> Metadatas { get; set; }

        /// <summary>
        /// Files collection
        /// </summary>
        [Required]
        public required IEnumerable<IFormFile> Files { get; set; }
    }
}

Akce v kontroleru:

using ApiExample.Dtos;
using Microsoft.AspNetCore.Mvc;

namespace ApiExample.Controllers
{
    public class SnapController : Controller
    {
        [HttpPost("create")]
        public IActionResult Create(SnapRequest snapRequest)
        {
            return Ok(snapRequest);
        }
    }
}

Výsledek ve Swaggeru vypadá takto:

Metadata se automaticky vložily do query, což může být problém, protože query je limitované maximální délkou (defaultně 260znaků). Všimněte si i toho, že když vyplníte například 2 metadata a vložíte 2 soubory po zpracování požadavku v akci kontroleru máte 0 metadat a 2 soubory 🙂

FormForm

Pojďme to torchu vylepšit. Query parametry jsou vhodné pro krétké informace, nejsou vhodné pro pole objektů. Pro pole objektů je bhodnější body, ale tam už máme soubory. Pošleme si tedy metadata přes FromForm:

using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

namespace ApiExample.Dtos
{
    public class SnapRequest
    {
        /// <summary>
        /// Metadata collection
        /// </summary>
        [Required]
        [FromForm]
        public required IEnumerable<Metadata> Metadatas { get; set; }

        /// <summary>
        /// Files collection
        /// </summary>
        [Required]
        [FromBody]
        public required IEnumerable<IFormFile> Files { get; set; }
    }
}

Kontroler:

using ApiExample.Dtos;
using Microsoft.AspNetCore.Mvc;

namespace ApiExample.Controllers
{
    public class SnapController : Controller
    {
        [HttpPost("create")]
        public IActionResult Create([FromForm] SnapRequest snapRequest)
        {
            return Ok(snapRequest);
        }
    }
}

Nyní už i Swagger vypadá trochu lépe. Všechny property se posálají v těle (body) kde máme podstatně vyšší limity než v query.

Ale stále nemáme splněno. Když zkusíte Swagger vyplnit, soubory jsou OK ale stále se do akce nedostanou žádná metada. Mám pro vás 2 možnosti řešení.

Request.Form

Problém spočívá v tom, že C# nedokáže rozparsrovat požadavek a napasovat ho na IEnumerable<Metadata>. Pojďme se podívat co nám přijde:

Ano, vidíte správně. Přišla nám informace:

{
  "name": "string1",
  "sjz": "string1"
},{
  "name": "string2",
  "sjz": "string2"
}

Vzpad8 to jako JSON ale není to JSON? Ano, přesně tak. Aby to byl JSON musely by na začátku a na konci být hranaté zévotky [ požadavek ]

Tím se dostáváme k prvnímu možnému řešení. Doplňme a začátek a konec záavorky a deseralizujme na kolekci objektů:

        [HttpPost("create")]
        public IActionResult Create([FromForm] SnapRequest snapRequest)
        {

            var metadatas = HttpContext.Request.Form["metadatas"];
            var metadataJson = $"[{metadatas}]";//Add [ to start a ] to end string -> it is true JSON array

            var options = new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            };
            //deserialize with camelCaseInsensitive option and save to request object
            snapRequest.Metadatas = JsonSerializer.Deserialize<List<Metadata>>(metadataJson, options) ?? new List<Metadata>();

            return Ok(snapRequest);
        }

ModelBinder

První řešení je spíše nouzové, lepší je si napsat vlastní ModelBinder, který tuto funkcionalitu zapouzdří.

Akce v kontroleru se velice zjednoduší:

        [HttpPost("v2/create")]
        public IActionResult CreateV2([FromForm] SnapRequestV2 snapRequest)
        {
            return Ok(snapRequest);
        }

DTO požadavku jsou rozšířené o ModelBiner:

using ApiExample.Controllers;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;

namespace ApiExample.Dtos
{
    public class SnapRequestV2
    {
        /// <summary>
        /// Metadata collection
        /// </summary>
        [Required]
        [FromForm]
        [ModelBinder(BinderType = typeof(FormDataJsonArrayModelBinder))]
        public required IEnumerable<Metadata> Metadatas { get; set; }

        /// <summary>
        /// Files collection
        /// </summary>
        [Required]
        [FromBody]
        public required IEnumerable<IFormFile> Files { get; set; }
    }
}

Vytvoření vlastního ModelBinderu v ASP.NET Core je užitečné, pokud potřebujete specifickou logiku pro vazbu dat z HTTP požadavku na objekty v rámci vaší aplikace. Vlastní ModelBinder vám umožní upravit proces vazby dat podle vašich potřeb. Vytvoříme třídu FormDataJsonArrayModelBinder implementující rozhraní IModelBinder:

    public class FormDataJsonArrayModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult != ValueProviderResult.None)
            {
                bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

                var stringValue = $"[{valueProviderResult.Values}]";
                var result = Newtonsoft.Json.JsonConvert.DeserializeObject(stringValue, bindingContext.ModelType);
                if (result != null)
                {
                    bindingContext.Result = ModelBindingResult.Success(result);
                    return Task.CompletedTask;
                }
            }

            return Task.CompletedTask;
        }
    }

Tato třída stejně jako v prvním řešení přidá na začátek a konec dat hranaté závorky a deserializuje je na požadovaný objekt.

C# Dynamický datový typ

Objčas potřebujeme udělat dynamicky malý objekt, kterému nastavíme parametry a pošleme ho dál. Ano, je to ošklivé, nemělo by se to dělat ale občas se to prostě hodí 🙂

dynamic flowCalculator = new
{
   Medium = inputData.HeaterEnum.ToString(),
   WaterTempIn = 80,
   WaterTempOut = 60

};

Jednoduše do toho dynamicky vygenerovaného objektu můžeme přidat další propertu:

if (inputData.MediumEnum == Medium.GLYCOL)
{
   flowCalculator.Concentration = inputData.Concentration;
}

System.Text.Json deserializace case insensitive

V C# objektu nazýváme property PascalCase s velkým písmenem na začátku.

    public class StudentDto
    {

        public long Id { get; set; }
        public string FirstName { get; set; }
        public double LastName { get; set; }
    }

Pokud na tento objekt chceme namapovat json, většinou narazíme na problém s rozdnou velikostí písmen a property se z jsonu nenamapují:

json:

{
    "id": 1,
    "firstName": "Jan",
    "lastName": "Novák"

}

Desertializace:

var student = JsonSerializer.Deserialize<Student>(jsonString);

Property v objektu zůstanou prázdné, protože nedošlo k namapování z jsonu (rozdílná velikost písmen)

Deserializace jsonu case insensitive

Serializeru můžeme nastavit propertu aby nerozlišoval velokost písmen:

var options = new JsonSerializerOptions 
{ 
    PropertyNameCaseInsensitive = true 
}; 

var student = JsonSerializer.Deserialize<Student>(jsonString, options);

Jak publikovat Windows 10 aplikaci do Store

Povedlo se mi publikovat mou první aplikaci ve Windows Store 🙂 Jedná se o můj klasický HelloWorld 🙂 https://www.microsoft.com/cs-cz/store/apps/motor-cb/9nblggh4v9ft

Tady jsou mé poznámky a poznatky k celému procesu.

Nejprve je nutné se přihlásit Microsoftím účtem: https://dev.windows.com/cs-cz

V řídícím panelu klikneme na tlačítko Vytvořit novou aplikaci :  (https://dev.windows.com/cs-cz/registration/AccountInfo). Při prvním spuštění toho kroku je nutné vyplnit údaje o autorovi aplikace a zaplatit jednorázový poplatek 365,-Kč (částka se ještě navýší o daň = cca 77,-Kč). Celkem tedy 442Kč. Platbu je možné provést přes kreditní kartu nebo PayPal.

Vytvořenou UWP (Universal Windows Platform) aplikaci a vyexportujeme ji přímo z Visual Studia. Klikneme na projekt pravým tlačítkem a vybereme Store ->Create App Packages … :

store1

store2

Poté je ještě nutné spustit App Certification Kit – jedná se o testy, které se samy vykonají a otestují základní funkčnost aplikace. Po dokončení těchto kroků máme k dispozici soubor s aplikací:

Documents\Visual Studio 2015\Projects\MotorCB\MotorCB\AppPackages\MotorCB_1.1.6.0_x86_x64_arm_bundle.appxupload

Ten nahrajeme na https://developer.microsoft.com/cs-cz/dashboard/overview a doplníme povinné údaje.

store3

A pak už jen čekat 🙂 Mou první aplikaci schválili za necelé 2 dny. Při tomto procesu se opět na aplikaci spouští testy.

Každý další upgrade aplikace je většinou schválen do 24hodin a do dalších 24 hodin se změny projeví ve Windows Store.

store4

Záseky při publikování aplikací

Šipka zpět

Tohle jsem nepochopil, ale programátor si musí ošetřit funkčnost šipky zpět 🙂 Úprava spočívá v přidání těchto řádků:

 namespace MotorCB
 {
     /// <summary>
     /// Provides application-specific behavior to supplement the default Application class.
     /// </summary>
     sealed partial class App : Application
     {
         /// <summary>
         /// Initializes the singleton application object.  This is the first line of authored code
         /// executed, and as such is the logical equivalent of main() or WinMain().
         /// </summary>
         public App()
         {
             Microsoft.ApplicationInsights.WindowsAppInitializer.InitializeAsync(
                 Microsoft.ApplicationInsights.WindowsCollectors.Metadata |
                 Microsoft.ApplicationInsights.WindowsCollectors.Session);
             this.InitializeComponent();
             this.Suspending += OnSuspending;
         }
 
         /// <summary>
         /// Invoked when the application is launched normally by the end user.  Other entry points
         /// will be used such as when the application is launched to open a specific file.
         /// </summary>
         /// <param name="e">Details about the launch request and process.</param>
         protected override void OnLaunched(LaunchActivatedEventArgs e)
         {
 #if DEBUG
             if (System.Diagnostics.Debugger.IsAttached)
             {
                 this.DebugSettings.EnableFrameRateCounter = false;
             }
 #endif
             Frame rootFrame = Window.Current.Content as Frame;
 
             // Do not repeat app initialization when the Window already has content,
             // just ensure that the window is active
             if (rootFrame == null)
             {
                 // Create a Frame to act as the navigation context and navigate to the first page
                 rootFrame = new Frame();
 
                 rootFrame.NavigationFailed += OnNavigationFailed;
+                rootFrame.Navigated += OnNavigated;
 
                 if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
                 {
                     //TODO: Load state from previously suspended application
                 }
 
                 // Place the frame in the current Window
                 Window.Current.Content = rootFrame;
+
+                // Register a handler for BackRequested events and set the
+                // visibility of the Back button
+                SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;
+
+                SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
+                    rootFrame.CanGoBack ?
+                    AppViewBackButtonVisibility.Visible :
+                    AppViewBackButtonVisibility.Collapsed;
             }
 
             if (e.PrelaunchActivated == false)
             {
                 if (rootFrame.Content == null)
                 {
                     // When the navigation stack isn't restored navigate to the first page,
                     // configuring the new page by passing required information as a navigation
                     // parameter
                     rootFrame.Navigate(typeof(MainPage), e.Arguments);
                 }
                 // Ensure the current window is active
                 Window.Current.Activate();
             }
         }
 
         /// <summary>
         /// Invoked when Navigation to a certain page fails
         /// </summary>
         /// <param name="sender">The Frame which failed navigation</param>
         /// <param name="e">Details about the navigation failure</param>
         void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
         {
             throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
         }
 
+        private void OnNavigated(object sender, NavigationEventArgs e)
+        {
+            // Each time a navigation event occurs, update the Back button's visibility
+            SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility =
+                ((Frame)sender).CanGoBack ?
+                AppViewBackButtonVisibility.Visible :
+                AppViewBackButtonVisibility.Collapsed;
+        }
+
         /// <summary>
         /// Invoked when application execution is being suspended.  Application state is saved
         /// without knowing whether the application will be terminated or resumed with the contents
         /// of memory still intact.
         /// </summary>
         /// <param name="sender">The source of the suspend request.</param>
         /// <param name="e">Details about the suspend request.</param>
         private void OnSuspending(object sender, SuspendingEventArgs e)
         {
             var deferral = e.SuspendingOperation.GetDeferral();
             //TODO: Save application state and stop any background activity
             deferral.Complete();
         }
+
+
+        private void OnBackRequested(object sender, BackRequestedEventArgs e)
+        {
+            Frame rootFrame = Window.Current.Content as Frame;
+
+            if (rootFrame.CanGoBack)
+            {
+                e.Handled = true;
+                rootFrame.GoBack();
+            }
+        }
     }
 }

Otevření URL ve webovém prohlížeči

String url = "http://blog.venca-x.cz"
await Launcher.LaunchUriAsync(new Uri(url));