github icontwitter icon

Test Test Diye Nice Nice: Mocklama (4) - React

Author imageEnes Başpınar /01 Ara 2022
20 min read •––– views

Bölümler

İçerik

Angular sevdiğimiz bir abimizdi. Karmaşık bir framework'tü, her şeyi yapmaya çalışırdı. Mesela ben yapmam. Bundle size'ı arttırırdı, ben arttırmam. Developerlar'ın işi düştüğünde sırtını dönerdi, ben dönmem. Angular sevdiğimiz bir abimizdi ama komponentleri de bir tuhaf render'lardı. Bizde hook'lar masaya konur. Herkes ihtiyacı kadarını alır. Eskiler sende kalsın kardeş, junior'ların kafasını karıştırma yeter.
- React

Mocklama serüvenimizde React'a kadar geldik. Çoğumuz React framework'ünü kullanıyoruz ve komponent testleriyle haşır neşir oluyoruz. Peki mocklama React komponentleriyle birlikte nasıl çalışır?

Yazının kodlarına Github üzerinden erişebilirsiniz.

Girizgah

Framework'lerle birlikte komponent dediğimiz özel fonksiyon türleri hayatımıza girer ve temel amaçları DOM elementri üretmek ve DOM ağacına render etmektir. Kullanıcının kayıtlı olup olmamasına göre formun içeriğini yöneten Registration komponentini ele alalım.

Registration.tsx
RegisterForm.tsx
LoginForm.tsx

_13
import React from "react";
_13
import LoginForm from "./LoginForm";
_13
import RegisterForm from "./RegisterForm";
_13
_13
const Registration: React.FC<RegistrationProps> = ({ isRegistered }) => {
_13
return isRegistered ? <LoginForm /> : <RegisterForm />;
_13
};
_13
_13
interface RegistrationProps {
_13
isRegistered: boolean;
_13
}
_13
_13
export default Registration;

Gördüğümüz üzere komponentler iç içe kullanılırlar ve uygulamanın giriş dosyasında özel bir metodla DOM ağacına render edilirler.


_10
import React from "react";
_10
import ReactDOM from "react-dom/client";
_10
import App from "./App";
_10
_10
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
_10
root.render(
_10
<React.StrictMode>
_10
<App />
_10
</React.StrictMode>
_10
);

İlgili dosya browser'da çalıştırıldığında DOM ağacı erişebilir haldedir. Ancak testlerimiz NodeJS ortamında çalıştığından ötürü ortada DOM ağacı yoktur. Komponentlerin test edilebilmesi için özel test kütüphaneleri tarafından sahte DOM ağacı oluşturulur. Makalede Jest ile birlikte React'tın en popüler test kütüphanesi olan React Testing Library kullanacağız.

Komponentleri mocklamak için önceki yazılardan faydalanabilir ve döndüreceği DOM içeriğini belirleyebiliriz. Genelde div döndürüp data-testid parametresi olarak komponent ismi vermeyi tercih ediyorum.

Registration.test.tsx

_15
import { render } from "@testing-library/react";
_15
import Registration from "./Registration";
_15
_15
jest.mock("./RegisterForm", () => {
_15
return {
_15
default: jest.fn().mockReturnValue(<div data-testid="register-form" />),
_15
};
_15
});
_15
_15
test("playground", () => {
_15
const { debug } = render(<Registration isRegistered={false} />);
_15
console.log("DOM:");
_15
// prints the HTML of the fake DOM tree at that stage of the test
_15
debug();
_15
});

=== Output:

DOM:

<body> <div> <div data-testid="register-form" /> </div> </body>

render metodu DOM ağacıyla yapabileceğimiz işlemler için metodlar barındıran bir nesne döndürür. Mock içeriklerin başarıyla render olduğunu görmek için
debug metodunu kullanabiliriz. Çıktıya baktığımızda istediğimiz değerin render edilmiş olduğunu görebiliriz. Komponentin başarıyla mocklandığını görmenin huzuruyla yolumuza devam edelim.

Komponent Mocklama

Hatırlarsanız test yazarkenki temel prensibimiz, testin odaklandığı fonksiyon dışındaki fonksiyonların mümkün mertebe çalıştırılmamasıydı. React'da ise child komponentlerin render edilmemesini ve unit test gerekliliklerinin karşılanmasını istiyoruz.

Test yazarken kendimize sormamız gereken soru "Bu komponentin görevi nedir?" olmalıdır. Registration komponentinin görevi, isRegistered props değerine göre hangi formun ekranda gösterileceğine karar vermektir. İçerisinde hangi inputlarin olduğunu ve işlem bitince yönlendirdiği sayfayı test etmemeliyiz. Bunlar formu render eden komponentin testinde ele alınmalıdır. data-testid değeriyle doğru formun render edilip edilmediğini kontrol edebiliriz.

Örnekleri basit tutmaya çalışacağım ve her seferinde farklı biçimde yazışlar göstereceğim ki kullanım örneklerini kafamızda çeşitlendirelim. Temel fikre odaklanırsanız kendi örneklerinize uygulayabileceğinizi düşünüyorum.

Testlerimizi yazalım.

jest.mock
jest.spyOn

_38
import { render, screen } from "@testing-library/react";
_38
import LoginForm from "./LoginForm";
_38
import RegisterForm from "./RegisterForm";
_38
import Registration from "./Registration";
_38
_38
jest.mock("./LoginForm", () => {
_38
return {
_38
default: jest.fn().mockReturnValue(<div data-testid="login-form" />),
_38
};
_38
});
_38
_38
jest.mock("./RegisterForm", () => {
_38
return {
_38
default: jest.fn().mockReturnValue(<div data-testid="register-form" />),
_38
};
_38
});
_38
_38
describe("<Registration /> tests", () => {
_38
afterEach(() => {
_38
jest.clearAllMocks();
_38
});
_38
_38
it("should render the register form when isRegistered is false", () => {
_38
render(<Registration isRegistered={false} />);
_38
_38
expect(RegisterForm).toHaveBeenCalled();
_38
expect(LoginForm).not.toHaveBeenCalled();
_38
expect(screen.getByTestId("register-form")).toBeInTheDocument();
_38
});
_38
_38
it("should render the login form when isRegistered is true", () => {
_38
render(<Registration isRegistered={true} />);
_38
_38
expect(LoginForm).toHaveBeenCalled();
_38
expect(RegisterForm).not.toHaveBeenCalled();
_38
expect(screen.getByTestId("login-form")).toBeInTheDocument();
_38
});
_38
});

=== Output:

PASS src/Registration.test.tsx <Registration> tests ✓ should render <RegisterForm /> when isRegistered equal false (57 ms) ✓ should render <LoginForm /> when isRegistered equal true (4 ms)

Mocklamalar test ihtiyaçlarına göre şekillendirilmelidir. Render edildiğini görmek istiyorsak basit bir data-testid işimizi görecektir.

Propsları Mock Komponente Geçme

Yapılacaklar listesi ve maddeleri için iki farklı komponentimiz olduğunu düşünelim.

App.tsx
TodoList.tsx
TodoItem.tsx

_29
import React from "react";
_29
import TodoList from "./TodoList";
_29
_29
function App() {
_29
const todos = [
_29
{
_29
title: "Ölümcül task'ı bitir.",
_29
isCompleted: false,
_29
},
_29
{
_29
title: "Yalandan iki spor yap.",
_29
isCompleted: false,
_29
},
_29
{
_29
title: "Yarım saat diye başlayıp 3 saat sürecek şekilde oyun oyna.",
_29
isCompleted: true,
_29
},
_29
{
_29
title: "Olmayan zamanını sosyal medyada öldür.",
_29
isCompleted: true,
_29
},
_29
];
_29
_29
return (
_29
<TodoList todos={todos} />
_29
);
_29
}
_29
_29
export default App;

Tamamlanan maddelerin ekranda gösterilmediğinden emin olmak istiyoruz.

TodoList.test.tsx

_38
import { render, screen } from "@testing-library/react";
_38
import TodoList from "./TodoList";
_38
_38
jest.mock("./TodoItem", () => jest.fn().mockReturnValue(
_38
<div data-testid="todo-item" />
_38
));
_38
_38
describe("<TodoList> tests", () => {
_38
test("should render only incomplete todo items", () => {
_38
const mockTodos = [
_38
{
_38
title: "Ölümcül task'ı bitir.",
_38
isCompleted: false,
_38
},
_38
{
_38
title: "Yalandan iki spor yap.",
_38
isCompleted: false,
_38
},
_38
{
_38
title: "Yarım saat diye başlayıp 3 saat sürecek şekilde oyun oyna.",
_38
isCompleted: true,
_38
},
_38
{
_38
title: "Olmayan zamanını sosyal medyada öldür.",
_38
isCompleted: true,
_38
},
_38
];
_38
_38
const { debug } = render(<TodoList todos={mockTodos} />);
_38
_38
console.log("DOM:");
_38
debug();
_38
_38
expect(screen.getAllByTestId("todo-item")).toHaveLength(2);
_38
expect(screen.getByText("Ölümcül task'ı bitir.")).toBeInTheDocument();
_38
expect(screen.getByText("Yalandan iki spor yap.")).toBeInTheDocument();
_38
});
_38
});

=== Output:

DOM: <body> <div> <div> <p>Yapılacaklar:</p> <div data-testid="todo-item" /> <div data-testid="todo-item" /> <div data-testid="todo-item" /> <div data-testid="todo-item" /> </div> </div> </body>

FAIL src/TodoList.test.tsx <TodoList> tests ✕ should render only incomplete todo items (83 ms) Expected length: 2 Received length: 4

Mock başarılı... ama bir problem var gibi. Tamamlanan maddeler de gösteriliyor ve hangi maddelerin render ettirildiğini belirten bir şey yok. Komponenti mockladığımızda render olmasını engelleyen koşul da kaybolur. Tamamlanma durumuna göre mock fonksiyonunun döndürdüğü değeri manipüle edebiliriz.

TodoList.test.tsx

_41
import { render, screen } from "@testing-library/react";
_41
import { TodoItemProps } from "./TodoItem";
_41
import TodoList from "./TodoList";
_41
_41
jest.mock("./TodoItem", () =>
_41
jest.fn(({ isCompleted, title }: TodoItemProps) => {
_41
return !isCompleted ? <div data-testid="todo-item">{title}</div> : null;
_41
})
_41
);
_41
_41
describe("<TodoList> tests", () => {
_41
test("should render only incomplete todo items", () => {
_41
const mockTodos = [
_41
{
_41
title: "Ölümcül task'ı bitir.",
_41
isCompleted: false,
_41
},
_41
{
_41
title: "Yalandan iki spor yap.",
_41
isCompleted: false,
_41
},
_41
{
_41
title: "Yarım saat diye başlayıp 3 saat sürecek şekilde oyun oyna.",
_41
isCompleted: true,
_41
},
_41
{
_41
title: "Olmayan zamanını sosyal medyada öldür.",
_41
isCompleted: true,
_41
},
_41
];
_41
_41
const { debug } = render(<TodoList todos={mockTodos} />);
_41
_41
console.log("DOM:");
_41
debug();
_41
_41
expect(screen.getAllByTestId("todo-item")).toHaveLength(2);
_41
expect(screen.getByText("Ölümcül task'ı bitir.")).toBeInTheDocument();
_41
expect(screen.getByText("Yalandan iki spor yap.")).toBeInTheDocument();
_41
});
_41
});

=== Output:

DOM: <body> <div> <div> <p>Yapılacaklar:</p> <div data-testid="todo-item">Ölümcül task'ı bitir.</div> <div data-testid="todo-item">Yalandan iki spor yap.</div> </div> </div> </body>

PASS src/TodoList.test.tsx <TodoList> tests ✓ should render only incomplete todo items (93 ms)

State'leri Manipüle Ettiğimiz Child Komponentleri Mocklama

Bazen komponent state değerlerini alt komponentlerden değiştirip yeniden render olmasını isteriz. Butona tıklandığında kişi listesinin çekildiği ve üst komponentte render edildiği bir senaryo olabilir.

PersonList.tsx
Person.tsx
FetchButton.tsx

_23
import React, { useState } from "react";
_23
import FetchButton from "./FetchButton";
_23
import Person from "./Person";
_23
_23
const PersonList: React.FC = () => {
_23
const [persons, setPersons] = useState<any>([]);
_23
_23
return (
_23
<div>
_23
<FetchButton setPersons={setPersons} />
_23
{persons.map((person: any) => (
_23
<Person
_23
key={person.email}
_23
firstName={person.firstName}
_23
lastName={person.lastName}
_23
email={person.email}
_23
/>
_23
))}
_23
</div>
_23
);
_23
};
_23
_23
export default PersonList;

Bu senaryoyu FetchButton komponentine ilettiğimiz setPersons setter metoduyla örneklendirebiliriz. Bunun için div elementine onClick olayı tanımlayıp dönüş değerini özelleştireceğiz.

PersonList.test.tsx

_54
import { fireEvent, render, screen } from "@testing-library/react";
_54
import { PersonProps } from "./Person";
_54
import * as FetchButtonModule from "./FetchButton";
_54
import PersonList from "./PersonList";
_54
_54
// div içeriğinin gerçekte nasıl olduğu hiç önemli değil.
_54
// doğru kişiyi render ettiğimizi teyit edebiliyorsak kafi.
_54
jest.mock("./Person", () =>
_54
jest.fn(({ firstName, lastName, email }: PersonProps) => (
_54
<div data-testid="person">
_54
{firstName} - {lastName} - {email}
_54
</div>
_54
))
_54
);
_54
_54
describe("<PersonList /> tests", () => {
_54
test("should get persons when click fetch button", () => {
_54
const mockPersons = [
_54
{
_54
firstName: "Terry",
_54
lastName: "Medhurst",
_54
email: "atuny0@sohu.com",
_54
},
_54
{
_54
firstName: "Sheldon",
_54
lastName: "Quigley",
_54
email: "hbingley1@plala.or.jp",
_54
},
_54
];
_54
_54
jest.spyOn(FetchButtonModule, "default")
_54
.mockImplementation(({ setPersons }: FetchButtonModule.FetchButtonProps) => (
_54
<button
_54
data-testid="fetch-button"
_54
onClick={() => setPersons(mockPersons)}
_54
/>
_54
));
_54
_54
render(<PersonList />);
_54
_54
expect(screen.queryAllByTestId("person")).toHaveLength(0);
_54
_54
const fetchButton = screen.getByTestId("fetch-button");
_54
fireEvent.click(fetchButton);
_54
_54
expect(screen.getAllByTestId("person")).toHaveLength(2);
_54
_54
screen.getAllByTestId("person").forEach((personEl, index) => {
_54
const { firstName, lastName, email } = mockPersons[index];
_54
_54
expect(personEl).toHaveTextContent(`${firstName} - ${lastName} - ${email}`);
_54
});
_54
});
_54
});

=== Output:

PASS src/PersonList.test.tsx <PersonList /> tests ✓ should get persons when click fetch button (36 ms)

PersonList komponentinin görevi state değerine göre kişileri render ettirmektir. Dolayısıyla alt komponentin listeyi nereden ve nasıl çektiğiyle ilgilenmez.

Kütüphane Komponentlerini Mocklama

Sayfalarımızın içeriği karmaşıklaştıkça yüklenme süresi şaha kalkar. Bu gibi durumlarda bildiğimiz üzere lazy load namı diğer tembel yükleme kullanırız. Adeta ödevini son güne bırakan tembel bir öğrenci misali komponentin ekranda gözükmesi yaklaşana kadar render etmez. Testleri yaptığımız NodeJS ortamında ekran dediğimiz kavram da olmadığı için komponentler hiçbir zaman render olmaz ve testleri gerçekleştiremeyiz.

Örneğimizi rastgele seçtiğim react-lazyload kütüphanesi üzerinden gösteriyor olacağım. Bunun yerine diğer herhangi bir kütüphane gelebilir.

Photos.tsx

_32
import axios from "axios";
_32
import { useEffect, useState } from "react";
_32
import LazyLoad from "react-lazyload";
_32
_32
const Photos: React.FC = () => {
_32
const [photos, setPhotos] = useState([]);
_32
_32
useEffect(() => {
_32
axios.get("https://jsonplaceholder.typicode.com/photos").then((result: any) => {
_32
setPhotos(result);
_32
});
_32
}, []);
_32
_32
return (
_32
<div>
_32
{photos.map((photo: any) => (
_32
<LazyLoad
_32
key={photo.id}
_32
height={600}
_32
>
_32
<img
_32
data-testid="photo"
_32
alt={photo.title}
_32
src={photo.url}
_32
/>
_32
</LazyLoad>
_32
))}
_32
</div>
_32
);
_32
};
_32
_32
export default Photos;

Testini yazalım.

Photos.test.tsx

_56
import { render, screen } from "@testing-library/react";
_56
import axios from "axios";
_56
import Photos from "./Photos";
_56
_56
describe("<Photos /> tests", () => {
_56
let mockPhotos: Array<{
_56
id: number,
_56
title: string,
_56
url: string,
_56
}>;
_56
_56
beforeAll(() => {
_56
mockPhotos = [
_56
{
_56
id: 1,
_56
title: "accusamus beatae ad facilis cum similique qui sunt",
_56
url: "https://via.placeholder.com/600/92c952",
_56
},
_56
{
_56
id: 2,
_56
title: "reprehenderit est deserunt velit ipsam",
_56
url: "https://via.placeholder.com/600/771796",
_56
},
_56
{
_56
id: 3,
_56
title: "officia porro iure quia iusto qui ipsa ut modi",
_56
url: "https://via.placeholder.com/600/24f355",
_56
},
_56
];
_56
_56
jest.spyOn(axios, "get").mockImplementation((url: string): any => {
_56
if (url === "https://jsonplaceholder.typicode.com/photos") {
_56
return Promise.resolve(mockPhotos);
_56
}
_56
_56
return Promise.reject();
_56
});
_56
});
_56
_56
test("should render greeting text without name", async () => {
_56
const { debug } = render(<Photos />);
_56
_56
// useEffect'teki fetch metodu ile veriyi çektikten sonra
_56
// state değerini güncelliyoruz. ancak bu durum meydana
_56
// gelmesini beklediğimiz render test içerisinde beklenmez.
_56
// ve eski dom içeriği görürüz. testi bekletmek adına `photo`
_56
// test id'li elementin render edilmesini bekleriz.
_56
const photoElements = await screen.findAllByTestId("photo");
_56
debug();
_56
_56
photoElements.forEach((photoEl, index) => {
_56
expect(photoEl).toHaveAttribute("alt", mockPhotos[index].title);
_56
expect(photoEl).toHaveAttribute("src", mockPhotos[index].url);
_56
});
_56
});
_56
});

=== Output:

<body> <div> <div> <div class="lazyload-wrapper"> <div class="lazyload-placeholder" style="height: 600px;" /> </div> <div class="lazyload-wrapper"> <div class="lazyload-placeholder" style="height: 600px;" /> </div> <div class="lazyload-wrapper"> <div class="lazyload-placeholder" style="height: 600px;" /> </div> </div> </div> </body>

FAIL src/Photos.test.tsx <Photos /> tests ✕ should render greeting text without name (1083 ms) wUnable to find an element by: [data-testid="photo"]

img elementi görmek yerine yer tutucuyla karşılaşıyoruz. NodeJS'de intersection observer olmadığından ötürü hiçbir zaman resim elementi DOM'a render olmaz. Bu şekilde testimizi gerçekleştiremeyeceğimizden dolayı LazyLoad komponentini mocklayacağız.

Photos.test.tsx

_61
import { render, screen } from "@testing-library/react";
_61
import axios from "axios";
_61
import * as ReactLazyLoadModule from "react-lazyload";
_61
import Photos from "./Photos";
_61
_61
describe("<Photos /> tests", () => {
_61
let mockPhotos: Array<{
_61
id: number,
_61
title: string,
_61
url: string,
_61
}>;
_61
_61
beforeAll(() => {
_61
mockPhotos = [
_61
{
_61
id: 1,
_61
title: "accusamus beatae ad facilis cum similique qui sunt",
_61
url: "https://via.placeholder.com/600/92c952",
_61
},
_61
{
_61
id: 2,
_61
title: "reprehenderit est deserunt velit ipsam",
_61
url: "https://via.placeholder.com/600/771796",
_61
},
_61
{
_61
id: 3,
_61
title: "officia porro iure quia iusto qui ipsa ut modi",
_61
url: "https://via.placeholder.com/600/24f355",
_61
},
_61
];
_61
_61
jest.spyOn(axios, "get").mockImplementation((url: string): any => {
_61
if (url === "https://jsonplaceholder.typicode.com/photos") {
_61
return Promise.resolve(mockPhotos);
_61
}
_61
_61
return Promise.reject();
_61
});
_61
});
_61
_61
test("should render greeting text without name", async () => {
_61
jest
_61
.spyOn(ReactLazyLoadModule, "default")
_61
.mockImplementation(({ children }: any) => children);
_61
_61
const { debug } = render(<Photos />);
_61
_61
// useEffect'teki fetch metodu ile veriyi çektikten sonra
_61
// state değerini güncelliyoruz. ancak bu durum meydana
_61
// gelmesini beklediğimiz render test içerisinde beklenmez.
_61
// ve eski dom içeriği görürüz. testi bekletmek adına `photo`
_61
// test id'li elementin render edilmesini bekleriz.
_61
const photoElements = await screen.findAllByTestId("photo");
_61
debug();
_61
_61
photoElements.forEach((photoEl, index) => {
_61
expect(photoEl).toHaveAttribute("alt", mockPhotos[index].title);
_61
expect(photoEl).toHaveAttribute("src", mockPhotos[index].url);
_61
});
_61
});
_61
});

=== Output:

<body> <div> <div> <img alt="accusamus beatae ad facilis cum similique qui sunt" data-testid="photo" src="https://via.placeholder.com/600/92c952" /> <img alt="reprehenderit est deserunt velit ipsam" data-testid="photo" src="https://via.placeholder.com/600/771796" /> <img alt="officia porro iure quia iusto qui ipsa ut modi" data-testid="photo" src="https://via.placeholder.com/600/24f355" /> </div> </div> </body>

PASS src/components/lazyload/Photos.test.tsx <Photos /> tests ✓ should render greeting text without name (55 ms)

Seri Sonu Canavarı

Yazının sonuna gelirken serideki bütün konuları birleştiren ve size challange olacak bir örnek koymak istedim. Geriye doğru sayma vazifesi icra eden mini React projesinin kodlarını aşağıya bıraktım. Buyrun testlerini yazmaya. Testin çıktısından faydalanabilirsiniz.

RemainingTimer.tsx
TimeBox.tsx
utils/date.ts

_52
import React, { useEffect, useState } from "react";
_52
import TimeBox from "./TimeBox";
_52
import { calculateDeltaBetweenDates, convertDeltaToDaysHoursMinutes } from "./utils/date";
_52
_52
const timerEndDate = new Date(2023, 5, 18, 17);
_52
_52
const RemainingTimer: React.FC = () => {
_52
const [remainingTime, setRemainingTime] = useState(calculateDeltaBetweenDates(timerEndDate));
_52
let remainingTimerInterval: any;
_52
_52
useEffect(() => {
_52
remainingTimerInterval = setInterval(() => {
_52
setRemainingTime(calculateDeltaBetweenDates(timerEndDate));
_52
}, 1000);
_52
_52
return () => {
_52
clearInterval(remainingTimerInterval);
_52
};
_52
}, []);
_52
_52
if (remainingTime === 0) {
_52
clearInterval(remainingTimerInterval);
_52
_52
return <div data-testid="timer-ended-text">Timer finished!</div>;
_52
}
_52
_52
const { remainingDays, remainingHours, remainingMinutes, remainingSeconds } =
_52
convertDeltaToDaysHoursMinutes(remainingTime);
_52
_52
return (
_52
<div data-testid="time-box-container">
_52
<TimeBox
_52
type="day"
_52
value={remainingDays}
_52
/>
_52
<TimeBox
_52
type="hour"
_52
value={remainingHours}
_52
/>
_52
<TimeBox
_52
type="min"
_52
value={remainingMinutes}
_52
/>
_52
<TimeBox
_52
type="sec"
_52
value={remainingSeconds}
_52
/>
_52
</div>
_52
);
_52
};
_52
_52
export default RemainingTimer;

=== Output:

PASS src/RemainingTimer.test.tsx <RemainingTimer /> tests ✓ should render time that returned convertDeltaToDaysHoursMinutes when delta greater than zero (79 ms) ✓ should render timer ended text when delta equal zero (41 ms)

Kapanış

React komponentlerine değindiğimiz bu yazıyla birlikte mocking serim sona erdi. Umarım açıklayıcı olmuştur. Testle ilgili İngilizce olarak bile çok büyük bir kaynak sıkıntısı varken bu yazının can suyu olmasını umuyorum. Sonuna kadar okuduysanız birkaç projenin testine giriştikten sonra yola alfa testçi olarak devam edebileceğinizi düşünüyorum. Seri hakkında geribildirimlerinizi duymaktan mutlu ve motive olurum.

Yazı burada biter. Sağlıcakla kalın.

Kaynaklar

2022 © No rights are reserved.Inspired by Lee Robinson's blog.