Prosty Kalkulator i testy jednostkowe

A. Struktura rozwiązania

Solution: ProstyKalkulatorPodTesty

  1. WPF AppProstyKalkulatorPodTesty (target: net8.0-windows)
    Foldery:
    • Modele
    • Logika
    • Walidacja
      Pliki systemowe: App.xaml, MainWindow.xaml, MainWindow.xaml.cs
  2. Testy ProstyKalkulatorPodTesty.Tests (target: net8.0)
    Referencja do projektu WPF (korzystamy z klas Modele/Logika/Walidacja).

B. Kod aplikacji

App.xaml

<Application x:Class="ProstyKalkulatorPodTesty.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <Style TargetType="TextBox">
            <Setter Property="Margin" Value="0,4,0,4"/>
        </Style>
        <Style TargetType="ComboBox">
            <Setter Property="Margin" Value="0,4,0,4"/>
        </Style>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="0,8,0,4"/>
            <Setter Property="MinWidth" Value="120"/>
            <Setter Property="Height" Value="34"/>
        </Style>
    </Application.Resources>
</Application>

MainWindow.xaml

<Window x:Class="ProstyKalkulatorPodTesty.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Prosty Kalkulator (MVVM)" Height="440" Width="540"
        WindowStartupLocation="CenterScreen">
    <Grid Margin="16">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>  <!-- A -->
            <RowDefinition Height="Auto"/>  <!-- B -->
            <RowDefinition Height="Auto"/>  <!-- Operacja -->
            <RowDefinition Height="Auto"/>  <!-- Oblicz + Wynik -->
            <RowDefinition Height="*"/>     <!-- Komunikaty + Historia -->
        </Grid.RowDefinitions>

        <!-- Wiersz 0: Liczba A -->
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="10"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <TextBlock Grid.Column="0" Text="Liczba A:" VerticalAlignment="Center"/>
            <TextBox Grid.Column="2"
                     Text="{Binding LiczbaA, UpdateSourceTrigger=PropertyChanged}"/>
        </Grid>

        <!-- Wiersz 1: Liczba B -->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="10"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <TextBlock Grid.Column="0" Text="Liczba B:" VerticalAlignment="Center"/>
            <TextBox Grid.Column="2"
                     Text="{Binding LiczbaB, UpdateSourceTrigger=PropertyChanged}"/>
        </Grid>

        <!-- Wiersz 2: Operacja -->
        <Grid Grid.Row="2">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="10"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <TextBlock Grid.Column="0" Text="Operacja:" VerticalAlignment="Center"/>
            <ComboBox Grid.Column="2"
                      ItemsSource="{Binding Operacje}"
                      SelectedItem="{Binding WybranaOperacja}"/>
        </Grid>

        <!-- Wiersz 3: Oblicz + wynik -->
        <StackPanel Grid.Row="3" Orientation="Horizontal" Margin="0,8,0,0">
            <Button Content="Oblicz" Command="{Binding ObliczPolecenie}" Margin="0,0,12,0"/>
            <TextBlock VerticalAlignment="Center">
                <Run Text="Wynik: "/>
                <!-- KLUCZOWE: WynikTekst jest tylko do odczytu -> Mode=OneWay -->
                <Run Text="{Binding WynikTekst, Mode=OneWay}"/>
            </TextBlock>
        </StackPanel>

        <!-- Wiersz 4: Komunikat + historia -->
        <StackPanel Grid.Row="4" Margin="0,10,0,0">
            <TextBlock Text="{Binding KomunikatBledu}" Foreground="Tomato" FontWeight="Bold"/>
            <TextBlock Text="Historia:" FontWeight="SemiBold" Margin="0,8,0,4"/>
            <ListView ItemsSource="{Binding Historia}">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Czas" Width="120" DisplayMemberBinding="{Binding CzasFormat}"/>
                        <GridViewColumn Header="Działanie" Width="240" DisplayMemberBinding="{Binding Opis}"/>
                        <GridViewColumn Header="Wynik" Width="120" DisplayMemberBinding="{Binding WynikFormat}"/>
                    </GridView>
                </ListView.View>
            </ListView>
        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs

using System.Windows;
using ProstyKalkulatorPodTesty.Logika;

namespace ProstyKalkulatorPodTesty;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new LogikaWidokModel(new LogikaKalkulator());
    }
}

Folder: Modele

ModeleOperacja.cs

namespace ProstyKalkulatorPodTesty.Modele;

public enum ModeleOperacja
{
    Dodawanie,
    Odejmowanie,
    Mnożenie,
    Dzielenie
}

ModeleDane.cs – wpis do historii

using System.Globalization;

namespace ProstyKalkulatorPodTesty.Modele;

public sealed class ModeleDane
{
    public DateTime Czas { get; init; } = DateTime.Now;
    public double A { get; init; }
    public double B { get; init; }
    public ModeleOperacja Operacja { get; init; }
    public double Wynik { get; init; }

    public string CzasFormat => Czas.ToString("HH:mm:ss");
    public string WynikFormat => Wynik.ToString(CultureInfo.CurrentCulture);

    public string Opis
    {
        get
        {
            var znak = Operacja switch
            {
                ModeleOperacja.Dodawanie   => "+",
                ModeleOperacja.Odejmowanie => "-",
                ModeleOperacja.Mnożenie    => "×",
                ModeleOperacja.Dzielenie   => "÷",
                _ => "?"
            };
            return $"{A.ToString(CultureInfo.CurrentCulture)} {znak} {B.ToString(CultureInfo.CurrentCulture)}";
        }
    }
}

Folder: Walidacja

WalidacjaKalkulator.cs

using System.Globalization;

namespace ProstyKalkulatorPodTesty.Walidacja;

public static class WalidacjaKalkulator
{
    public static bool SprobujParsowac(string? tekstA, string? tekstB,
                                       out double a, out double b, out string komunikatBledu)
    {
        a = 0; b = 0; komunikatBledu = string.Empty;

        if (string.IsNullOrWhiteSpace(tekstA) || string.IsNullOrWhiteSpace(tekstB))
        {
            komunikatBledu = "Podaj obie liczby.";
            return false;
        }

        var culture = CultureInfo.CurrentCulture;

        if (!double.TryParse(tekstA.Trim(), NumberStyles.Float, culture, out a))
        {
            komunikatBledu = $"Niepoprawna liczba A: „{tekstA}”.";
            return false;
        }

        if (!double.TryParse(tekstB.Trim(), NumberStyles.Float, culture, out b))
        {
            komunikatBledu = $"Niepoprawna liczba B: „{tekstB}”.";
            return false;
        }

        return true;
    }

    public static bool SprawdzDzielenie(double b, out string komunikatBledu)
    {
        if (b == 0)
        {
            komunikatBledu = "Nie można dzielić przez zero.";
            return false;
        }
        komunikatBledu = string.Empty;
        return true;
    }
}

Folder: Logika

LogikaKalkulator.cs

using ProstyKalkulatorPodTesty.Modele;

namespace ProstyKalkulatorPodTesty.Logika;

public class LogikaKalkulator
{
    public double Oblicz(double a, double b, ModeleOperacja operacja)
    {
        return operacja switch
        {
            ModeleOperacja.Dodawanie   => a + b,
            ModeleOperacja.Odejmowanie => a - b,
            ModeleOperacja.Mnożenie    => a * b,
            ModeleOperacja.Dzielenie   => a / b, // 0 jest weryfikowane w walidacji
            _ => throw new NotSupportedException("Nieznana operacja.")
        };
    }
}

LogikaPolecenie.cs (ICommand)

using System;
using System.Windows.Input;

namespace ProstyKalkulatorPodTesty.Logika;

public sealed class LogikaPolecenie : ICommand
{
    private readonly Action _wykonaj;
    private readonly Func<bool>? _czyMozna;

    public LogikaPolecenie(Action wykonaj, Func<bool>? czyMozna = null)
    {
        _wykonaj = wykonaj;
        _czyMozna = czyMozna;
    }

    public bool CanExecute(object? parameter) => _czyMozna?.Invoke() ?? true;
    public void Execute(object? parameter) => _wykonaj();

    public event EventHandler? CanExecuteChanged;
    public void OdświeżCanExecute() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

LogikaWidokModel.cs (ViewModel)

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using ProstyKalkulatorPodTesty.Modele;
using ProstyKalkulatorPodTesty.Walidacja;

namespace ProstyKalkulatorPodTesty.Logika;

public sealed class LogikaWidokModel : INotifyPropertyChanged
{
    private readonly LogikaKalkulator _kalkulator;

    public LogikaWidokModel(LogikaKalkulator kalkulator)
    {
        _kalkulator = kalkulator;
        Operacje = new ObservableCollection<ModeleOperacja>(
            (ModeleOperacja[])Enum.GetValues(typeof(ModeleOperacja))
        );
        WybranaOperacja = ModeleOperacja.Dodawanie;
        ObliczPolecenie = new LogikaPolecenie(Oblicz);
    }

    // Wejście
    private string _liczbaA = string.Empty;
    public string LiczbaA { get => _liczbaA; set { _liczbaA = value; OnPropertyChanged(); } }

    private string _liczbaB = string.Empty;
    public string LiczbaB { get => _liczbaB; set { _liczbaB = value; OnPropertyChanged(); } }

    public ObservableCollection<ModeleOperacja> Operacje { get; }
    private ModeleOperacja _wybranaOperacja;
    public ModeleOperacja WybranaOperacja { get => _wybranaOperacja; set { _wybranaOperacja = value; OnPropertyChanged(); } }

    // Wyjście
    private double? _wynik;
    public string WynikTekst => _wynik.HasValue ? _wynik.Value.ToString(CultureInfo.CurrentCulture) : "-";

    private string _komunikatBledu = string.Empty;
    public string KomunikatBledu { get => _komunikatBledu; set { _komunikatBledu = value; OnPropertyChanged(); } }

    public ObservableCollection<ModeleDane> Historia { get; } = new();

    // Polecenia
    public ICommand ObliczPolecenie { get; }

    private void Oblicz()
    {
        KomunikatBledu = string.Empty;
        _wynik = null; OnPropertyChanged(nameof(WynikTekst));

        if (!WalidacjaKalkulator.SprobujParsowac(LiczbaA, LiczbaB, out var a, out var b, out var blad))
        {
            KomunikatBledu = blad;
            return;
        }

        if (WybranaOperacja == ModeleOperacja.Dzielenie &&
            !WalidacjaKalkulator.SprawdzDzielenie(b, out var bladDzielenia))
        {
            KomunikatBledu = bladDzielenia;
            return;
        }

        var wynik = _kalkulator.Oblicz(a, b, WybranaOperacja);
        _wynik = wynik; OnPropertyChanged(nameof(WynikTekst));

        Historia.Insert(0, new ModeleDane
        {
            A = a,
            B = b,
            Operacja = WybranaOperacja,
            Wynik = wynik
        });
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? nazwa = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nazwa));
}

C. Testy jednostkowe (MSTest)

Projekt: ProstyKalkulatorPodTesty.Tests
(Dołącz MSTest.TestFramework i MSTest.TestAdapter jeśli szablon nie dodał.)

LogikaKalkulatorTests.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using ProstyKalkulatorPodTesty.Logika;
using ProstyKalkulatorPodTesty.Modele;

namespace ProstyKalkulatorPodTesty.Tests
{
    [TestClass]
    public class LogikaKalkulatorTests
    {
        private readonly LogikaKalkulator _kalkulator = new();

        [TestMethod]
        public void Dodawanie_Daje_Poprawny_Wynik()
        {
            double wynik = _kalkulator.Oblicz(2, 3, ModeleOperacja.Dodawanie);
            Assert.AreEqual(5, wynik);
        }

        [TestMethod]
        public void Odejmowanie_Daje_Poprawny_Wynik()
        {
            double wynik = _kalkulator.Oblicz(10, 4, ModeleOperacja.Odejmowanie);
            Assert.AreEqual(6, wynik);
        }

        [TestMethod]
        public void Mnozenie_Daje_Poprawny_Wynik()
        {
            double wynik = _kalkulator.Oblicz(2, 5, ModeleOperacja.Mnożenie);
            Assert.AreEqual(10, wynik);
        }

        [TestMethod]
        public void Dzielenie_Daje_Poprawny_Wynik()
        {
            double wynik = _kalkulator.Oblicz(9, 3, ModeleOperacja.Dzielenie);
            Assert.AreEqual(3, wynik);
        }
    }
}

WalidacjaKalkulatorTests.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using ProstyKalkulatorPodTesty.Walidacja;

namespace ProstyKalkulatorPodTesty.Tests
{
    [TestClass]
    public class WalidacjaKalkulatorTests
    {
        [TestMethod]
        public void Parsowanie_Poprawnych_Wartosci_Przechodzi()
        {
            // Uwaga: używamy bieżącej kultury – w PL separator to przecinek.
            bool ok = WalidacjaKalkulator.SprobujParsowac("1,5", "2", out double a, out double b, out string err);
            Assert.IsTrue(ok);
            Assert.AreEqual(1.5, a, 1e-9);
            Assert.AreEqual(2.0, b, 1e-9);
            Assert.AreEqual(string.Empty, err);
        }

        [TestMethod]
        public void Parsowanie_Tekstu_Z_Spacji_Dziala()
        {
            bool ok = WalidacjaKalkulator.SprobujParsowac("  3  ", "  4,2  ", out double a, out double b, out string err);
            Assert.IsTrue(ok);
            Assert.AreEqual(3.0, a, 1e-9);
            Assert.AreEqual(4.2, b, 1e-9);
            Assert.AreEqual(string.Empty, err);
        }

        [TestMethod]
        public void Puste_Pole_Zwraca_Blad()
        {
            bool ok = WalidacjaKalkulator.SprobujParsowac("", "2", out _, out _, out string err);
            Assert.IsFalse(ok);
            StringAssert.Contains(err, "Podaj obie liczby");
        }

        [TestMethod]
        public void Niepoprawna_Liczba_A_Zwraca_Blad()
        {
            bool ok = WalidacjaKalkulator.SprobujParsowac("abc", "2", out _, out _, out string err);
            Assert.IsFalse(ok);
            StringAssert.Contains(err, "Niepoprawna liczba A");
        }

        [TestMethod]
        public void Niepoprawna_Liczba_B_Zwraca_Blad()
        {
            bool ok = WalidacjaKalkulator.SprobujParsowac("2", "xyz", out _, out _, out string err);
            Assert.IsFalse(ok);
            StringAssert.Contains(err, "Niepoprawna liczba B");
        }

        [TestMethod]
        public void Dzielenie_Przez_Zero_Wykryte()
        {
            bool ok = WalidacjaKalkulator.SprawdzDzielenie(0, out string err);
            Assert.IsFalse(ok);
            StringAssert.Contains(err, "Nie można dzielić przez zero");
        }

        [TestMethod]
        public void Dzielenie_Nie_Przez_Zero_Prawidlowe()
        {
            bool ok = WalidacjaKalkulator.SprawdzDzielenie(2, out string err);
            Assert.IsTrue(ok);
            Assert.AreEqual(string.Empty, err);
        }
    }
}


D. Szybka instrukcja w VS 2022

  1. Utwórz WPF App ProstyKalkulatorPodTesty (Framework: .NET 8, Windows).
  2. Dodaj foldery Modele, Logika, Walidacja.
  3. Wklej powyższe pliki do odpowiednich folderów.
  4. Utwórz projekt testowy MSTest: ProstyKalkulatorPodTesty.Tests (net8.0) i dodaj referencję do projektu WPF.
  5. Testy → Uruchom wszystkie testy.
  6. Uruchom aplikację.