ch13-react-router

Heroes bileşenimiz, rotaları kullanmaya ihtiyaç duyuyor, ancak uygulamamızda bunu henüz ayarlamadık. Şu ana kadar, her bileşen izole bir şekilde tasarlandı. Bu arada, gerçek uygulama genel bir sayfada başlatılır ve kullanıcı bununla çok fazla işlem yapamaz. Bu bölümde react-router ayarlayacağız ve bunu test etmek için e2e kullanacağız.

react-router-dom ve react-router-native react-router içindedir. Web uygulaması üzerinde çalıştığımız için react-router-dom'u react-router olarak adlandıracağız.

e2e Kullanımı

Bileşenlerin içinde gezinme bağlantıları olan bileşenlerde, rotalama özelliğinin bir kısmını bileşen testinde test edebiliriz. HeaderBar ve NavBar bileşenlerinde bunların örneklerini uyguladık. Bunun ötesinde, rotayı test etmenin en güvenilir yolu e2e testler kullanmaktır çünkü uygulamanın rotalama özelliklerini ve olası akışlarını tamamen kapsamamızı sağlar.

e2e koşucusunu yarn cy:open-e2e ile başlatıyoruz. Bu komut, localhost:3000 adresinde uygulamaya hizmet veren yarn start komutunu da çalıştırır. Şu anda, spec dosyasını çalıştırırken genel React uygulamasını görüyoruz. Bunu routes-nav.cy.ts olarak yeniden adlandırabiliriz.

Gereksinimimiz, uygulamamızı sunmak, bağlantılara tıklamak, doğru URL'lere gitmek ve ilgili bileşenleri işlemektir.

Henüz Villains bileşenini uygulamadık, şimdilik bu sorun değil.

Yukarıdan aşağıya doğru başlayarak, ilk başarısız testimiz HeaderBar bileşenini işlemek için (Kırmızı 1).

// cypress/e2e/routes-nav.cy.ts
describe("e2e sanity", () => {
  it("passes sanity", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
  });
});

Diğer bileşenleri içeren bir bileşeni test ettiğimizde, çocuk bileşenin kaynağına ve bileşen testine bakmak gibi en iyi bir uygulama vardır. e2e ile uygulamayı test ederken de aynıdır. src/components/HeaderBar.cy.tsx, HeaderBar'ı monte ederken BrowserRouter ile sarmalar, bu da ana uygulamamızın da BrowserRouter'a ihtiyacı olduğu anlamına gelir (Yeşil 1).

// src/App.tsx
import HeaderBar from "components/HeaderBar";
import { BrowserRouter } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
    </BrowserRouter>
  );
}

export default App;

İkinci gereklilik NavBar bileşenini göstermektir (Kırmızı 2).

// cypress/e2e/routes-nav.cy.ts
describe("e2e sanity", () => {
  it("should render header bar and nav bar", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
    cy.getByCy("nav-bar").should("be.visible");
  });
});

NavBar />'ı uygulamamıza ekleyin ve test geçiyor (Yeşil 2).

// src/App.tsx
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import { BrowserRouter } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <NavBar />
    </BrowserRouter>
  );
}

export default App;

Özgün uygulamadan bazı css ekleyerek işlemeyi daha da düzgün hale getirebiliriz (Düzenleme 2).

// src/App.tsx
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import { BrowserRouter } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <div className="section columns">
        <NavBar />
      </div>
    </BrowserRouter>
  );
}
export default App;

NavBar.cy.tsx'ye baktığımızda kahramanlar, kötü adamlar ve hakkında tıklama navigasyonunu zaten kapsadığımızı görüyoruz. Bu testi e2e'de tekrarlamamıza gerek yok. Daha düşük seviyedeki testlerin kapsamını her zaman kontrol edin ve daha yüksek seviyede çaba harcamaktansa çabayı çoğaltmamayı tercih edin, çünkü bu daha fazla maliyet getirebilir ama ekstra güven sağlamayabilir.

e2e veya bileşen testleri kullanıyor olsanız bile, TDD'nin akışı aynıdır; başarısız olan bir şeyle başlayın, çalışacak şekilde en aza indirgeyin ve ardından daha iyi hale getirin. Ana ayrım ölçektir; e2e ile daha küçük artımlı adımlara sahip olmak için daha dikkatli olmamız gerekiyor çünkü uygulamanın büyük ölçekteki etkisi daha yüksek olabilir ve başarısızlıkları teşhis etmeyi daha zor hale getirebilir. Test odaklı tasarımda uygulaması zor olan açık uygulama, bir seferde çok küçük artımlı testler yazmaktır.

Yönlendirme

Mevcut olmayan bir rotayı ziyaret ettiğimizde NotFound bileşenini işleyip işlemediğimizi kontrol eden başarısız bir test yazın (Kırmızı 3).

// cypress/e2e/routes-nav.cy.ts
describe("e2e sanity", () => {
  it("should render header bar and nav bar", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
    cy.getByCy("nav-bar").should("be.visible");
  });
  it("should land on not found when visiting an non-existing route", () => {
    cy.visit("/route48");
    cy.getByCy("not-found").should("be.visible");
  });
});

react-router kullanmak için bileşenimizi bir Routes bileşeniyle sarmalamanız ve içe aktarmanız gerekir. Her bileşen, bir Route bileşenindeki bir element özelliği olur. Bileşeni bir path özelliğine eşleriz. * ise diğer yollara uymayan her şeyin buna yönlendirileceği anlamına gelir (Yeşil 3).

// src/App.tsx
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import NotFound from "components/NotFound";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <div className="section columns">
        <NavBar />
        <Routes>
          <Route path="*" element={<NotFound />} />
        </Routes>
      </div>
    </BrowserRouter>
  );
}
export default App;

react-router kullanarak render'ı biraz daha geliştirebiliriz (Düzenleme 3).

// src/App.tsx
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import NotFound from "components/NotFound";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <div className="section columns">
        <NavBar />
        <main className="column">
          <Routes>
            <Route path="*" element={<NotFound />} />
          </Routes>
        </main>
      </div>
    </BrowserRouter>
  );
}

export default App;

About bileşenini ekleyerek yol ayarını biraz daha ilginç hale getirelim. Aşağıdakileri src/About.tsx'ye kopyalayın.

// src/About.tsx
import React from "react";

const About = () => (
  <div data-cy="about" className="content-container">
    <div className="content-title-group not-found">
      <h2 className="title">Tour of Heroes</h2>
      <p>
        This project was created to provide a perspective on Test Driven Design
        using Cypress component and e2e testing to develop a React application.
        There are many versions of Angular's Tour of Heroes tutorial and John
        Papa has re-created them in Angular, Vue and React. The 3 apps are
        consistent in their styles and design decisions. This one inspires from
        them, uses CCTDD and takes variances along the way.
      </p>

      </br>
      <h2 className="title">Live applications by John Papa</h2>

      <ul>
        <li>
          <a href="https://papa-heroes-angular.azurewebsites.net">
            Tour of Heroes with Angular
          </a>
        </li>
        <li>
          <a href="https://papa-heroes-react.azurewebsites.net">
            Tour of Heroes with React
          </a>
        </li>
        <li>
          <a href="https://papa-heroes-vue.azurewebsites.net">
            Tour of Heroes with Vue
          </a>
        </li>
      </ul>
    </div>
  </div>
);

export default About;

Şimdi yola doğrudan gitmeyi test eden başarısız bir test yazabiliriz. NavBar bileşen testinde tıklama-nav sürümünü zaten yazdık ve bunu e2e'de tekrar etmiyoruz (Kırmızı 4).

// cypress/e2e/routes-nav.cy.ts
describe("e2e sanity", () => {
  it("should render header bar and nav bar", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
    cy.getByCy("nav-bar").should("be.visible");
  });
  it("should land on not found when visiting an non-existing route", () => {
    cy.visit("/route48");
    cy.getByCy("not-found").should("be.visible");
  });

  it("should direct-navigate to about", () => {
    cy.visit("/about");
    cy.getByCy("about").contains("CCTDD");
  });
});

About bileşenini /about yoluna ayarlayarak testi geçerli hale getiriyoruz (Yeşil 4).

// src/App.tsx
import About from "About";
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import NotFound from "components/NotFound";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <div className="section columns">
        <NavBar />
        <main className="column">
          <Routes>
            <Route path="/about" element={<About />} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </main>
      </div>
    </BrowserRouter>
  );
}

export default App;

test eklemeyi düşünmek istiyoruz. URL kontrollerini ekleyerek testleri, bir url'ye yönlendirirken bileşen render'ının yanı sıra destekleyebiliriz (Düzenleme 4).

// cypress/e2e/routes-nav.cy.ts
describe("e2e sanity", () => {
  it("should render header bar and nav bar", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
    cy.getByCy("nav-bar").should("be.visible");
  });
  it("should land on not found when visiting an non-existing route", () => {
    const route = "/route48";
    cy.visit(route);
    cy.location('pathname').should('eq', route);
    cy.getByCy("not-found").should("be.visible");
  });

  it("should direct-navigate to about", () => {
    const route = "/about";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("about").contains("CCTDD");
  });
});

Uygulamamız için varsayılan URL'nin ne olması gerektiği konusunda bir soru sormak istiyoruz. En karmaşık bileşen Heroes olduğundan, bu uygun bir seçimdir. Boş bir yola gidildiğinde /heroes yoluna yönlendirilmek ve Heroes bileşenini görüntülemek istiyoruz. Bu ihtiyaca yönelik başarısız bir test ekleyelim (Kırmızı 5).

// cypress/e2e/routes-nav.cy.ts
describe("e2e sanity", () => {
  it("should render header bar and nav bar", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
    cy.getByCy("nav-bar").should("be.visible");

    cy.location('pathname').should('eq' "heroes");
  });
  it("should land on not found when visiting an non-existing route", () => {
    const route = "/route48";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("not-found").should("be.visible");
  });

  it("should direct-navigate to about", () => {
    const route = "/about";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("about").contains("CCTDD");
  });
});

react-routerda bu özelliği kullanmanın yolu Navigate bileşenini kullanmaktır. Şimdi, yönlendirilebilecek /heroes yoluna da ihtiyacımız var (Yeşil 5).

// src/App.tsx
import About from "About";
import HeaderBar from "components/HeaderBar";
import NavBar from "components/NavBar";
import NotFound from "components/NotFound";
import Heroes from "heroes/Heroes";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import "./styles.scss";

function App() {
  return (
    <BrowserRouter>
      <HeaderBar />
      <div className="section columns">
        <NavBar />
        <main className="column">
          <Routes>
            <Route path="/" element={<Navigate replace to="/heroes" />} />
            <Route path="/heroes" element={<Heroes />} />
            <Route path="/about" element={<About />} />
            <Route path="*" element={<NotFound />} />
          </Routes>
        </main>
      </div>
    </BrowserRouter>
  );
}

export default App;

İlk testi, HeaderBar ve NavBarın tüm yönlendirme testlerinde doğru olduğunu kontrol eden testi değiştirebiliriz. Burada, yönlendirmeye yönelik yeni bir test yazmak yerine testi değiştirmek tercih edilir. Mevcut testi değiştirmek için fırsatlar arayın, yeni özellikler için kısmen yinelenen testler yazmak yerine. Bir test açısından önemli olan şey, bir testin başlangıç durumudur; bu duruma ulaşmak ortaksa, o zaman bu test iyileştirmesi için bir fırsattır ve kısmi test çoğaltma yerine. Ayrıca /heroes rotası için doğrudan gezinme işlevselliğini kontrol eden yeni bir test ekleyebiliriz (Düzenleme 5).

// cypress/e2e/routes-nav.cy.ts
describe("Routes and navigation", () => {
  it("should land on baseUrl, redirect to /heroes", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
    cy.getByCy("nav-bar").should("be.visible");

    cy.location('pathname').should('eq' "/heroes");
    cy.getByCy("heroes").should("be.visible");
  });

  it("should direct-navigate to /heroes", () => {
    const route = "/heroes";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("heroes").should("be.visible");
  });

  it("should land on not found when visiting an non-existing route", () => {
    const route = "/route48";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("not-found").should("be.visible");
  });

  it("should direct-navigate to about", () => {
    const route = "/about";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("about").contains("CCTDD");
  });
});

Bu noktada başka hangi testleri düşünebiliriz? Rota geçmişi hakkında ne dersiniz? Bunun için düşük maliyetli ve güvenilir bir e2e testi ile kapsayabileceğimiz bir test ekleyebiliriz. Testi, kahramanlar -> kötü adamlar -> hakkında'dan farklı bir rota sırası kullanarak daha ilginç hale getirebiliriz (Düzenleme 5).

// cypress/e2e/routes-nav.cy.ts
describe("e2e sanity", () => {
  it("should land on baseUrl, redirect to /heroes", () => {
    cy.visit("/");
    cy.getByCy("header-bar").should("be.visible");
    cy.getByCy("nav-bar").should("be.visible");

    cy.location('pathname').should('eq' "/heroes");
    cy.getByCy("heroes").should("be.visible");
  });

  it("should direct-navigate to /heroes", () => {
    const route = "/heroes";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("heroes").should("be.visible");
  });

  it("should land on not found when visiting an non-existing route", () => {
    const route = "/route48";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("not-found").should("be.visible");
  });

  it("should direct-navigate to about", () => {
    const route = "/about";
    cy.visit(route);
    cy.location('pathname').should('eq' route);
    cy.getByCy("about").contains("CCTDD");
  });

  it("should cover route history with browser back and forward", () => {
    cy.visit("/");
    const routes = ["villains", "heroes", "about"];
    cy.wrap(routes).each((route: string) =>
      cy.get(`[href="/${route}"]`).click()
    );

    const lastIndex = routes.length - 1;
    cy.location('pathname').should('eq' routes[lastIndex]);
    cy.go("back");
    cy.location('pathname').should('eq' routes[lastIndex - 1]);
    cy.go("back");
    cy.location('pathname').should('eq' routes[lastIndex - 2]);
    cy.go("forward").go("forward");
    cy.location('pathname').should('eq' routes[lastIndex]);
  });
});

Bileşen testi kullanma

Doğrudan gezinme ve yönlendirme ile ilgili testler konusunda e2e ile çok güveniyoruz. Ayrıca src/components/NotFound.cy.tsx dosyasında tıklama ile gezinmeyi de ele aldık. Bir bileşen testinde, url bağlanma sırasında mevcut değildir ve cy.visit kullanamayız. Ancak tıklama ile gezinme kullanabiliriz. App.cy.tsx dosyasını bu şekilde güncelleyebiliriz. Cypress koşucusundan bileşen testine geçin veya yarn cy:open-ct ile başlatın.

import App from "./App";

describe("ct sanity", () => {
  it("should render the App", () => {
    cy.mount(<App />);
    cy.getByCy("not-found").should("be.visible");

    cy.getByCy("nav-bar").within(() => {
      cy.contains("p", "Menu");

      const routes = ["heroes", "villains", "about"];
      cy.getByCy("menu-list").children().should("have.length", routes.length);

      routes.forEach((route: string) => {
        cy.get(`[href="/${route}"]`)
          .contains(route, { matchCase: false })
          .click()
          .should("have.class", "active-link")
          .siblings()
          .should("not.have.class", "active-link");

        cy.location("pathname").should("eq", route);
      });
    });
  });
});

Bağlanma işlemi sırasında url'nin belirsiz olduğunu cy.getByCy('not-found').should('be.visible') kullanarak kontrol ediyoruz. Geri kalan test, src/components/NotFound.cy.tsx dosyasından kopyalanan ve yapıştırılan bir kısımdır. Bunun yerine, App bileşeninin alt bileşenlerinin render'ını kontrol edebiliriz.

import App from "./App";

describe("ct sanity", () => {
  it("should render the App", () => {
    cy.mount(<App />);
    cy.getByCy("not-found").should("be.visible");

    cy.contains("Heroes").click();
    cy.getByCy("heroes").should("be.visible");

    cy.contains("About").click();
    cy.getByCy("about").should("be.visible");
  });
});

Bu test, mevcut e2e ve bileşen testlerinin üzerinde fazladan bir güven sağlamaz çünkü ekstra bir şey yapmaz. En iyisi, akıl sağlığı testi olarak hizmet edebilir. Şimdilik Cypress bileşen testini React Testing Library ile karşılaştıran yan projede saklayacağız. İşte aynı testin başlangıç RTL kopyası.

// src/App.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import App from "./App";

test("renders tour of heroes", async () => {
  render(<App />);

  await userEvent.click(screen.getByText("About"));
  expect(screen.getByTestId("about")).toBeVisible();

  await userEvent.click(screen.getByText("Heroes"));
  expect(screen.getByTestId("heroes")).toBeVisible();
});

// CT vs RTL: src/App.cy.tsx

src/setupTests.ts dosyasını güncelleyin ve varsayılan test kimliği seçiciyi data-cy olarak yeniden yazın.

// src/setupTests.ts
import "@testing-library/jest-dom";

import { configure } from "@testing-library/react";

configure({ testIdAttribute: "data-cy" });

Birim testini yarn test ile çalıştırın.

Özet

Uygulama hizmete sunulduğunda ana bileşenlerin bazılarını oluşturup oluşturamadığımızı kontrol etmek için bir e2e testi yazdık (Kırmızı 1, Kırmızı 2)

Testleri geçmek için ana bileşeni saran BrowserRouter ekledik (Yeşil 1, Yeşil 2).

Stil ekledik (Düzenleme 2).

Geçersiz bir rota için başarısız olan bir test ekledik ve NotFound bileşenini oluşturduk (Kırmızı 3).

Rota kurulumunun temelini oluşturduk; NotFound öğesi olan Route bileşenini saran Routes bileşeni (Yeşil 3).

Stil ekledik (Düzenleme 3).

/about rotaları için doğrudan bir navigasyon testi ekledik (Kırmızı 4).

About bileşeni için rotayı ayarladık (Yeşil 4).

Uygulamanın başlangıç yönlendirmesini /heroes'tan (Kırmızı 5) kontrol etmek için bir test ekledik.

Navigate ile rotaların ayarını geliştirdik (Yeşil 5).

Doğrudan /heroes'a yönlendirmeyi kontrol etmek için bir test ekledik ve rota geçmişini test etmek için başka bir test ekledik (Düzenleme 5).

App.cy.tsx adlı testi inceledik ve test çoğaltma konusunu tartıştık. Testin, başka bir bileşen testi veya e2e testinden başka bir şey yapmadığı halde, mantıklı olduğuna karar verdik. Başka bir neden ise, CT ile RTL arasındaki 1: 1 karşılaştırmalarını incelemeye başlamaktı. App RTL testini, App mantıklı bileşen testini yansıtacak şekilde güncelledik.

Çıkarılacak Dersler

  • E2e testi, uygulamanın yönlendirme özelliklerini ve olası akışları daha iyi bir şekilde kapsamamıza olanak tanır.

  • E2e veya bileşen testleri kullanıyor olun, TDD'nin temel fikri aynıdır; başarısız olan bir şeyle başlayın, çalışmasını sağlamak için minimumu yapın ve ardından daha iyi hale getirin.

  • Test odaklı tasarımda uygulamaya açık ama zor olan uygulama, bir seferde çok küçük artımlı testler yazmaktır. Değişikliklerin daha yüksek etki yarıçapı nedeniyle, e2e testlerle daha küçük artışları daha dikkatli değerlendirin.

  • Başka bileşenleri içeren bir bileşeni test ettiğimizde, çocuk bileşen kaynağına ve bileşen testine bakın. Aynı kural e2e için de geçerlidir. Daha düşük seviyeli testlerin kapsamını her zaman kontrol edin ve çaba sarf etmeyi yüksek seviyede tekrarlamaktan kaçının, çünkü ekstra maliyeti olacaktır ancak ekstra güvence sağlamayabilir.

  • Geçerli testlerimiz olduğunda, daha fazla kaynak kodu eklemekten önce refactor yapmayı veya daha fazla test eklemeyi tercih ederiz.

  • Zaten mevcut olan testi değiştirmek için fırsatlar arayın, yeni özellikler için kısmen yinelenen testler yazmak yerine. Bir test açısından önemli olan şey, bir testin başlangıç durumudur; bu duruma ulaşmak yaygın ise, bu, testin iyileştirilmesi veya kısmi test çoğaltması açısından bir fırsattır.

Last updated