Что такое наследование в ООП?

Ответ

Наследование — это фундаментальный принцип объектно-ориентированного программирования, который позволяет одному классу (дочернему классу, подклассу) перенимать (наследовать) поля и методы другого класса (родительского класса, суперкласса). Это создает иерархию «is-a» (является) и способствует повторному использованию кода (DRY — Don't Repeat Yourself).

Прямое применение в автоматизации тестирования: Наследование широко используется в тестовых фреймворках для создания базовых тестовых классов, которые содержат общую конфигурацию, утилиты и шаги подготовки данных.

Пример: Базовый класс для UI-тестов с использованием Selenium WebDriver

// Базовый (родительский) класс для всех UI-тестов.
public class BaseUITest {
    // Защищенное (protected) поле, чтобы наследники имели к нему доступ.
    protected WebDriver driver;
    protected WebDriverWait wait;

    // Метод, который будет выполнен перед каждым тестом в наследниках.
    @BeforeEach
    public void setUp() {
        // Инициализация драйвера (например, Chrome)
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    // Общий метод для логина, который могут использовать все тесты.
    protected void login(String username, String password) {
        driver.findElement(By.id("username")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);
        driver.findElement(By.id("login-btn")).click();
    }

    // Общий метод для скриншота при падении.
    protected void takeScreenshot(String testName) {
        File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        try {
            FileUtils.copyFile(screenshot, new File("screenshots/" + testName + ".png"));
        } catch (IOException e) { /* обработка ошибки */ }
    }

    // Метод, который будет выполнен после каждого теста в наследниках.
    @AfterEach
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
}

// Дочерний (наследующий) класс для конкретного функционального теста.
// Он наследует ВСЕ non-private поля и методы BaseUITest.
class LoginPageTest extends BaseUITest { // Ключевое слово 'extends'

    @Test
    void testSuccessfulLogin() {
        // Arrange: Используем унаследованный драйвер и wait.
        driver.get("https://myapp.com/login");

        // Act: Используем унаследованный метод login().
        login("validUser", "validPass123");

        // Assert: Пишем специфичные для этого теста проверки.
        wait.until(ExpectedConditions.urlContains("/dashboard"));
        String welcomeText = driver.findElement(By.cssSelector(".welcome-msg")).getText();
        assertTrue(welcomeText.contains("validUser"));
    }

    @Test
    void testLoginWithInvalidPassword() {
        driver.get("https://myapp.com/login");
        login("validUser", "wrong");

        String errorMessage = driver.findElement(By.id("error")).getText();
        assertEquals("Invalid credentials", errorMessage);
    }

    // Мы можем ПЕРЕОПРЕДЕЛИТЬ (override) метод родителя, если нужно особое поведение.
    @Override
    @AfterEach
    public void tearDown() {
        takeScreenshot(this.getClass().getSimpleName()); // Сначала делаем скриншот
        super.tearDown(); // Затем вызываем оригинальный метод родителя для закрытия драйвера
    }
}

Преимущества для QA-инженера:

  1. Устранение дублирования: Общий код (инициализация драйвера, логин, фикстуры) пишется один раз в базовом классе.
  2. Согласованность: Все тесты запускаются в одинаковых предварительных условиях.
  3. Упрощение поддержки: Изменение в общей логике (например, способа инициализации драйвера) вносится в одном месте — базовом классе.
  4. Создание специализированных иерархий: Можно создать BaseAPITest, BaseMobileTest, наследуя от них более конкретные тестовые классы.

Важно: Следует отдавать предпочтение композиции перед наследованием, если отношения между классами не строго подчиняются принципу «является». В тестировании наследование уместно именно для организации фреймворка и общих фикстур.

Ответ 18+ 🔞

А, ну вот, наследование! Это ж классика, ёпта, как водка под селёдку. Представь, что у тебя есть старый дедовский чемодан — там уже лежат все базовые инструменты: отвёртка, молоток, пассатижи. И ты не таскаешь этот набор по каждому новому объекту отдельно, а просто берёшь этот чемодан и добавляешь в него специфичный для задачи инструмент. Вот это и есть наследование в ООП: один класс (родительский) отдаёт свои поля и методы другому (дочернему), а тот может ещё и своё прикрутить. Создаётся чёткая иерархия «является» (is-a). DRY-принцип, не повторяйся, блядь, святое дело для ленивого, но умного программиста.

Где это впиливается в автоматизацию? Да везде, сука! Чаще всего — для создания базовых тестовых классов, куда скидывают всю общую хуйню: конфигурацию драйвера, утилитные методы, подготовку данных. Чтобы не писать одно и то же в каждом втором тесте, ядрёна вошь.

Смотри, как это выглядит на практике: Базовый класс для UI-тестов на Selenium

// Это наш дедовский чемодан, базовый класс для всех UI-тестов.
public class BaseUITest {
    // Делаем поля protected, чтобы дети-наследники могли к ним лапами потыкать.
    protected WebDriver driver;
    protected WebDriverWait wait;

    // Метод, который отработает ПЕРЕД каждым тестом у наследников.
    @BeforeEach
    public void setUp() {
        // Инициализируем драйвер (допустим, Chrome)
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    // Общий метод для логина. Чтоб в каждом тесте заново селекторы не прописывать.
    protected void login(String username, String password) {
        driver.findElement(By.id("username")).sendKeys(username);
        driver.findElement(By.id("password")).sendKeys(password);
        driver.findElement(By.id("login-btn")).click();
    }

    // Общий метод для скриншота на память, когда тест накрылся медным тазом.
    protected void takeScreenshot(String testName) {
        File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
        try {
            FileUtils.copyFile(screenshot, new File("screenshots/" + testName + ".png"));
        } catch (IOException e) { /* тут обработка ошибки, но нам да похуй в примере */ }
    }

    // Метод, который отработает ПОСЛЕ каждого теста. Чистим за собой.
    @AfterEach
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }
}

// А вот наш наследник, конкретный тестовый класс.
// Ключевое слово 'extends' — это как сказать: «Папа, дай чемодан!».
class LoginPageTest extends BaseUITest {

    @Test
    void testSuccessfulLogin() {
        // Arrange: Драйвер и wait уже есть, они унаследованы! Красота.
        driver.get("https://myapp.com/login");

        // Act: Используем метод login() от родителя. Вообще не паримся.
        login("validUser", "validPass123");

        // Assert: А вот тут уже наша специфичная проверка.
        wait.until(ExpectedConditions.urlContains("/dashboard"));
        String welcomeText = driver.findElement(By.cssSelector(".welcome-msg")).getText();
        assertTrue(welcomeText.contains("validUser"));
    }

    @Test
    void testLoginWithInvalidPassword() {
        driver.get("https://myapp.com/login");
        login("validUser", "wrong");

        String errorMessage = driver.findElement(By.id("error")).getText();
        assertEquals("Invalid credentials", errorMessage);
    }

    // А вот тут магия: мы можем ПЕРЕОПРЕДЕЛИТЬ (override) родительский метод, если его поведение нас не устраивает.
    @Override
    @AfterEach
    public void tearDown() {
        takeScreenshot(this.getClass().getSimpleName()); // Сначала делаем свой скриншот
        super.tearDown(); // А потом вызываем родительский метод, чтобы драйвер закрылся
    }
}

Что мы, как QA, с этого имеем, кроме морального удовлетворения?

  1. Нет дублирования кода. Всё общее — в одном месте. Поддержка — одно удовольствие, а не пиздец.
  2. Единообразие. Все тесты стартуют в одинаковых условиях, доверия к результатам — овердохуища.
  3. Лёгкость поддержки. Захотел поменять браузер на Firefox? Идёшь в один-единственный setUp() в базовом классе и меняешь. Во всех наследниках автоматом подтянется. Волнение ебать как снижается.
  4. Чистая архитектура. Можно наделать разных базовых классов: BaseAPITest для API, BaseMobileTest для мобилки. И от них плодить специализированные тесты.

Но! Не злоупотребляй, чувак. Если отношения между классами не «является», а больше «имеет в составе», то лучше использовать композицию. Наследование — мощный инструмент, но для организации именно каркаса фреймворка и общих подготовок. А то получится хитрая жопа из десяти уровней наследования, в которой через полгода никто не разберётся.