Testy Jednostkowe C# Aplikacja

0

Witam.
Posiadam taką prostą aplikacje, która ma kilka funkcji (dodawanie osoby, usuwanie osoby, aktualizacja danych osoby, czyszczenie pól, wyszukiwanie osoby w bazie).
Potrzebuję zrobić powiedzmy 3 testy jednostkowe. Problem w tym, że nie wiem jak to zrobić. Co mają sprawdzać testy? Na przykład czy dana osoba została dodana do bazy lub usunięta?
Przeszukałem już sporo stron i filmików na yt, ale nadal nie wiem jak się za to zabrać.

using EasyConnect.easyconnectClasses;
using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Windows.Forms;


namespace EasyConnect
{

    public partial class EasyConnect : Form
    {
        public EasyConnect()
        {
            InitializeComponent();
        }
        ConnectClass c = new ConnectClass();
        private void EasyConnect_Load(object sender, EventArgs e)
        {
            DataTable dt = c.Select();
            dataGridViewContactList.DataSource = dt;
        }

        private void buttonAdd_Click(object sender, EventArgs e)
        {
            //Pobierz wartość z pola wejściowego
            c.FirstName = textBoxFirstName.Text;
            c.LastName = textBoxLastName.Text;
            c.ContactNo = textBoxContactNumber.Text;
            c.Address = textBoxAddress.Text;
            c.Gender = comboBoxGender.Text;

            bool success = c.Insert(c);
            if(success==true)
            {
                MessageBox.Show("Nowy kontakt został pomyślnie wprowadzony.");
                Clear();
            }
            else
            {
                MessageBox.Show("Błąd w dodawaniu nowego kontaktu. Spróbuj ponownie.");
            }

            DataTable dt = c.Select();
            dataGridViewContactList.DataSource = dt;

        }

        private void pictureBox1_Click(object sender, EventArgs e)
        {
            this.Close();
        }

        //Metoda do czyszczenia danych po lewej stronie
        public void Clear()
        {
            textBoxContactID.Text = "";
            textBoxFirstName.Text = "";
            textBoxLastName.Text = "";
            textBoxContactNumber.Text = "";
            textBoxAddress.Text = "";
            comboBoxGender.Text = "";
        }

        private void buttonUpdate_Click(object sender, EventArgs e)
        {
            c.ContactID = int.Parse(textBoxContactID.Text);
            c.FirstName = textBoxFirstName.Text;
            c.LastName = textBoxLastName.Text;
            c.ContactNo = textBoxContactNumber.Text;
            c.Address = textBoxAddress.Text;
            c.Gender = comboBoxGender.Text;

            bool success = c.Update(c);
            if(success == true)
            {
                MessageBox.Show("Kontakt został pomyślnie zaktualizowany.");
                DataTable dt = c.Select();
                dataGridViewContactList.DataSource = dt;
                Clear();
            }
            else
            {
                MessageBox.Show("Błąd w aktualizacji kontaktu. Spróbuj ponownie.");
            }

        }

        private void dataGridViewContactList_RowHeaderMouseClick(object sender, DataGridViewCellMouseEventArgs e)
        {
            int rowIndex = e.RowIndex;
            textBoxContactID.Text = dataGridViewContactList.Rows[rowIndex].Cells[0].Value.ToString();
            textBoxFirstName.Text = dataGridViewContactList.Rows[rowIndex].Cells[1].Value.ToString();
            textBoxLastName.Text = dataGridViewContactList.Rows[rowIndex].Cells[2].Value.ToString();
            textBoxContactNumber.Text = dataGridViewContactList.Rows[rowIndex].Cells[3].Value.ToString();
            textBoxAddress.Text = dataGridViewContactList.Rows[rowIndex].Cells[4].Value.ToString();
            comboBoxGender.Text = dataGridViewContactList.Rows[rowIndex].Cells[5].Value.ToString();
        }

        private void buttonClear_Click(object sender, EventArgs e)
        {
            //Wywołaj metode czyszczenia
            Clear();
        }

        private void buttonDelete_Click(object sender, EventArgs e)
        {
            c.ContactID = Convert.ToInt32(textBoxContactID.Text);
            bool success = c.Delete(c);
            if(success==true)
            {
                MessageBox.Show("Kontakt został pomyślnie usunięty.");
                DataTable dt = c.Select();
                dataGridViewContactList.DataSource = dt;
                Clear();
            }
            else
            {
                MessageBox.Show("Błąd w usuwaniu kontaktu. Spróbuj ponownie.");
            }
        }

        static string myconnectionString = ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString;

        public object DragGable { get; private set; }

        private void textBoxSearch_TextChanged(object sender, EventArgs e)
        {
            string keyword = textBoxSearch.Text;

            SqlConnection conn = new SqlConnection(myconnectionString);
            SqlDataAdapter sda = new SqlDataAdapter("SELECT * FROM Table_Contact WHERE FirstName LIKE '%" + keyword + "%' OR LastName LIKE '%" + keyword + "%' OR Address LIKE '%" + keyword + "%'",conn);
            DataTable dt = new DataTable();
            sda.Fill(dt);
            dataGridViewContactList.DataSource = dt;
        }

        bool mouseDown;
        private Point offset;

        private void panel1_MouseDown(object sender, MouseEventArgs e)
        {
            offset.X = e.X;
            offset.Y = e.Y;
            mouseDown = true;
        }

        private void panel1_MouseMove(object sender, MouseEventArgs e)
        {
            if (mouseDown == true)
            {
                Point currentScreenPos = PointToScreen(e.Location);
                Location = new Point(currentScreenPos.X - offset.X, currentScreenPos.Y - offset.Y);
            }
        }

        private void panel1_MouseUp(object sender, MouseEventArgs e)
        {
            mouseDown = false;
        }
    }
}

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EasyConnect.easyconnectClasses
{
    class ConnectClass
    {
        public int ContactID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string ContactNo { get; set; }
        public string Address { get; set; }
        public string Gender  { get; set; }

        static string myconnectionString = ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString;

        public DataTable Select()
        {
            ///Krok 1: Połączenie bazy
            SqlConnection conn = new SqlConnection(myconnectionString);
            DataTable dt = new DataTable();
            try
            {
                //Krok 2: Tworzenie zapytania SQL
                string sql = "SELECT * FROM Table_Contact";

                //Tworzenie cmd przy użyciu sql i conn
                SqlCommand cmd = new SqlCommand(sql, conn);

                //Tworzenie SQL DataAdapter przy użyciu cmd
                SqlDataAdapter adapter = new SqlDataAdapter(cmd);
                conn.Open();
                adapter.Fill(dt);
            }
            catch (Exception)
            {

            }
            finally
            {
                conn.Close();
            }
            return dt;
        }
        //Wstawianie danych do bazy danych
        public bool Insert (ConnectClass c)
        {
            //Tworzenie domyślnej intrukcji powrotu i ustawienie jej wartości na fałsz
            bool isSucces = false;

            //Krok 1: Połączenie bazy danych
            SqlConnection conn = new SqlConnection(myconnectionString);
            try
            {
                //Krok 2: Tworzenie zapytania SQL aby wstawic dane
                string sql = "INSERT INTO Table_Contact (FirstName, LastName, ContactNo, Address, Gender) VALUES (@FirstName, @LastName, @ContactNo, @Address, @Gender)";

                //Tworzenie komendy SQL przy użyciu sql i conn
                SqlCommand cmd = new SqlCommand(sql, conn);

                //Tworzenie parametrów
                cmd.Parameters.AddWithValue("@FirstName", c.FirstName);
                cmd.Parameters.AddWithValue("@LastName", c.LastName);
                cmd.Parameters.AddWithValue("@ContactNo", c.ContactNo);
                cmd.Parameters.AddWithValue("@Address", c.Address);
                cmd.Parameters.AddWithValue("@Gender", c.Gender);

                //Otwarte połączenie z bazą danych
                conn.Open();
                int rows = cmd.ExecuteNonQuery();

                //Jeżeli zapytanie będzie prawdziwe to wartość bedzie wieksza od 0, w przeciwnym wypadku wartosc bedzie 0.
                if (rows > 0)
                {
                    isSucces = true;
                }
                else
                {
                    isSucces = false;
                }
            }
            catch(Exception)
            {

            }
            finally
            {
                conn.Close();
            }
            return isSucces;
        }

        //Metoda do aktualizowania danych w naszej bazie
        public bool Update(ConnectClass c)
        {
            //Tworzenie domyślnej intrukcji powrotu i ustawienie jej wartości na fałsz
            bool isSuccess = false;

            //Krok 1: Połączenie bazy danych
            SqlConnection conn = new SqlConnection(myconnectionString);
            try
            {
                //Tworzenie zapytania SQL do aktualizacji danych w bazie
                string sql = "UPDATE Table_Contact SET FirstName=@FirstName, LastName=@LastName, ContactNo=@ContactNo, Address=@Address, Gender=@Gender WHERE ContactID=@ContactID";

                //Tworzenie komendy SQL
                SqlCommand cmd = new SqlCommand(sql, conn);

                //Tworzenie parametrów w celu dodania wartości
                cmd.Parameters.AddWithValue("@FirstName", c.FirstName);
                cmd.Parameters.AddWithValue("@LastName", c.LastName);
                cmd.Parameters.AddWithValue("@ContactNo", c.ContactNo);
                cmd.Parameters.AddWithValue("@Address", c.Address);
                cmd.Parameters.AddWithValue("@Gender", c.Gender);
                cmd.Parameters.AddWithValue("@ContactID", c.ContactID);

                //Otwarte połączenie z bazą danych
                conn.Open();

                int rows = cmd.ExecuteNonQuery();

                //Jeżeli zapytanie będzie prawdziwe to wartość bedzie wieksza od 0, w przeciwnym wypadku wartosc bedzie 0.
                if (rows > 0)
                {
                    isSuccess = true;
                }
                else
                {
                    isSuccess = false;
                }
            }
            catch (Exception)
            {

            }
            finally
            {
                conn.Close();
            }
            return isSuccess;
        }

        //Metoda do usuwania danych z bazy danych
        public bool Delete(ConnectClass c)
        {
            //Tworzenie domyślnej intrukcji powrotu i ustawienie jej wartości na fałsz
            bool isSuccess = false;

            //Krok 1: Połączenie bazy danych
            SqlConnection conn = new SqlConnection(myconnectionString);
            try
            {
                //Tworzenie zapytania SQL do usuwania danych w bazie
                string sql = "DELETE FROM Table_Contact WHERE ContactID=@ContactID";

                //Tworzenie komendy SQL
                SqlCommand cmd = new SqlCommand(sql, conn);

                //Tworzenie parametrów w celu dodania wartości
                cmd.Parameters.AddWithValue("@ContactID", c.ContactID);

                //Otwarte połączenie z bazą danych
                conn.Open();

                int rows = cmd.ExecuteNonQuery();

                //Jeżeli zapytanie będzie prawdziwe to wartość bedzie wieksza od 0, w przeciwnym wypadku wartosc bedzie 0.
                if (rows > 0)
                {
                    isSuccess = true;
                }
                else
                {
                    isSuccess = false;
                }
            }
            catch (Exception)
            {

            }
            finally
            {
                conn.Close();
            }
            return isSuccess;
        }
    }
}

```C#
3

W tym rzecz, że nie stosujesz żadnego wzorca projektowego.
Efekt jest taki, że wszystko: widok (kontrolki, reakcje), model (baza danych, obiekty w systemie) oraz całą logikę wrzucasz do jednego worka. To powoduje, że ciężko Ci znaleźć to, co tak naprawdę masz testować.
Zapoznaj się na przykład z MVVM, skoro to desktop.

Gdy oddzielisz widok od modeli bardzo szybko pojawi Ci się to, co tak naprawdę należy testować: Czy operacje zrobione na obiekcie są wykonane poprawnie.
Nie testuj (a już na pewno nie jednostkowo) poprawności zapisu/odczytu do bazy danych - to inny zakres testów.

4

Generalnie tak. Rozdziel to jak mówi poprzednik. Gdzie indziej powinny być modele, gdzie indziej widoki. Jeśli nie używasz żadnego ORMa, to zapoznaj się ze wzorcem "Repozytorium".

I kluczem do testowania są... Interfejsy. Załóżmy taki prosty przykład. Jeśli użytkownik poda zbyt krótkie hasło podczas rejestracji, nie może być dodany do bazy. Jeśli poda odpowiednie, to jest ok.

Program mógłby wyglądać tak.

Model:

public class User: DbItem //jakiś DbItem, który ma np. tylko Id
{
    public string UserName { get; set; }
    public string Password { get; set; }
}

Następnie masz interfejs (nie do końca jest to poprawne repozytorium, ale uprościłem dla przykładu):

public interface IUserRepository
{
    void AddToDb(User user);
}

Następnie masz klasę repozytorium - tutaj cała zabawa z bazą danych:

public class UserRepository: IUserRepository
{
    SqlConnection conn;

   public void AddToDb(User user)
   {
       string sql = "INSERT INTO users(username, password) VALUES(param1, param2)";
       //itd
   }
}

I na koniec masz... miejsce, w którym odbywa się cała logika - czyli serwis -> on też może implementować interfejs

public class UserService: IUserService
{
    IUserRepository repo;
  
    public UserService(IUserRepository repo)
    {
        this.repo = repo;
    }

    public bool RegisterUser(User user)
    {
        //i tu leci logika:
        if(user.Password.Length < 7)
            return false;
        else
        {
            repo.AddToDb(user);
            return true;
        }
    } 
}

Zwróć uwagę, że to jest mocno okrojona implementacja. Normalnie pewnie zwróciłbyś jakiś inny wynik niż bool. Np. jakiś rezultat operacji z komunikatem błędu i AddToDb byłoby w try..catch

No dobra, teraz z poziomu Twojej formy możesz tego użyć:

public class MyForm: Form
{
    IUserService userService;

    public MyForm(IUserService userService)
    {
        this.userService = userService
    }

    public void OkButtonClick()
    {
        if(userService.RegisterUser(theUser))
            MessageBox.Show("Użytkownik dodany");
        else
            MessageBox.Show("Za krótkie hasło.... albo coś");
    }
}

No dobra, masz już z grubsza program. Teraz pytanie -> jak to testować? I co w ogóle?
Otóż pierwszą zasadą jest - testuj tylko swój kod.

Co to znaczy? Czy będziesz testował repozytorium? Nie. A dlaczego? Bo tam masz tak naprawdę tylko wrzutkę do bazy danych. Czyli wykonanie polecenia INSERT. To zostało już przetestowane przez twórców biblioteki. A więc Ty już tego nie testujesz. Testujesz tylko swoją logikę. Jaką masz tu logikę? Sprawdzenie, czy hasło użytkownika jest odpowiednie (uwaga, w .NetCore są już do tego przetestowane metody, wziąłem to tylko dlatego, że jest to proste).

A więc co testujesz? Testujesz swój serwis.

A więc piszemy klasę testów (na przykładzie NUnit).

[TestFixture]
public class UserServiceTests
{
    [Test]
    public void RegisterUser_BadPassword_ReturnsFalse()
    {
    }

    [Test]
    public void RegisterUser_GoodPassword_ReturnsTrue()
    {

    }
} 

To jest szkielet Twojej klasy do testowania. Jak widzisz testujemy tutaj dwa warunki. Zwróć uwagę na nazewnictwo metod. To jest jedna z przyjętych konwencji. CoTestujesz_JakieWarunki_JakiWynikSpodziewany

I teraz przychodzi odpowiedź "Po co interfejsy".
Testy jednostkowe mają za zadanie przetestować konkretną metodę w konkretnych warunkach. Nie powinny robić żadnych zmian w bazie danych - w ogóle nie powinny niczego fizycznie zapisywać ani zmieniać.

Dlatego repozytorium implementuje interfejs.
Teraz możesz sobie zamockować ten interfejs. Poczytaj do czego służy biblioteka Moq. W dużym skrócie - ona automagicznie potrafi stworzyć obiekt implementujący dany interfejs i zwracający to, co chcesz. Tutaj zrobimy prościej - posłużymy się "Fakiem".

A więc stwórzmy oszukane repozytorium:

class FakeUserRepo: IUserRepository
{
   public void AddToDb(User user)
   {
       //załóż, że operacja się powiodła, niczego nie rób
   }
}

Oczywiście jesli AddToDb zwracałoby jakąś wartość, to musiałbyś też tu jakąs zwrócić. Po prostu - na pałę, bez ingerencji w bazę danych.

Wróćmy do klasy testowej i uzupełnijmy ją:

[TestFixture]
public class UserServiceTests
{
    [Test]
    public void RegisterUser_BadPassword_ReturnsFalse()
    {
        User user = new User();
        user.Username = "Testowy";
        user.Password = "abc"; // hasło za krótkie

        UserService service = new UserService(new FakeUserRepo());
        bool result;
        Assert.DoesNotThrow(() => result = service.RegisterUser(user));
        Assert.IsFalse(result);
    }

    [Test]
    public void RegisterUser_GoodPassword_ReturnsTrue()
    {
        User user = new User();
        user.Username = "Testowy";
        user.Password = "abc123456"; // hasło ok

        UserService service = new UserService(new FakeUserRepo());
        bool result;
        Assert.DoesNotThrow(() => result = service.RegisterUser(user));
        Assert.IsTrue(result);

    }
} 

Jaki jest rezultat? W tym momencie testujesz prawdziwą implementację serwisu użytkownika - UserService. Ale UserService nie używa tutaj bazy danych, tylko Twojego fake'owego repo. Czyli wszystko jest w porządku. Masz tutaj sprawdzić, czy logika w UserService zadziała. Czyli - za krótkie hasło - nie dodawaj, hasło ok - dodaj użytkownika.

Assert.DoesNotThrow - upewnia się, że kod przekazany w parametrze nie wywali wyjątku
Assert.IsTrue/Assert.IsFalse - chyba nie muszę mówić :)

Tak to mniej więcej powinno wyglądać. To taka podstawa, od której możesz wyjść

1 użytkowników online, w tym zalogowanych: 0, gości: 1