JUnit 5 의 아키텍쳐
작성자 : eDell.LEE
Java Ecosystem 내에서 가장 인기있는 단위 테스트 프레임워크 로써 아래의 3개의 서브 프로젝트로 구성 되어 있다.
- JUnit Platform
- JUnit Jupiter
- JUnit Vintage
Platform은 JVM 위에서 테스트 프레임워크를 가동시킨다.
Jupiter는 테스트 코드 작성을 위한 어노테이션이 포함 되어 있다.
마지막으로 Vintage 는 하위 모듈 (JUnit3, JUnit4) 테스트 실행을 지원한다.
Jupiter는 테스트 코드 작성을 위한 어노테이션이 포함 되어 있다.
마지막으로 Vintage 는 하위 모듈 (JUnit3, JUnit4) 테스트 실행을 지원한다.
테스트 실행 환경 구성
JDK 1.8 이상
JUnit 5 이상
IntelliJ 2018

즉, JUnit4 에서 작성한 코드는 Vintage 엔진에 의해 Platform을 통해서 구동되며, Jupiter API 호출하는 테스트는 Jupiter 엔진을 통해 Platform 의 TestEngine을 통해 테스트 케이스를 실행하게 된다.
현재 (2018.05) GA 버전은 5.2.0 이다
Gradle 설정은 아래와 같다.
dependencies {
testCompile('org.junit.jupiter:junit-jupiter-api:5.2.0')
testRuntime('org.junit.jupiter:junit-jupiter-engine:5.2.0')
}
Annotation
Jupiter
에서 지원하는 테스트 구성 어노테이션은 아래와 같다.Annotation Discription
Annotation | Description |
---|---|
@Test | 테스트 메소드임을 선언 |
@ParameterizedTest | 매개 변수를 통해 여러 인수를 테스함 |
@RepeatedTest | 메소드 반복 테스트 |
@TestFactory | 메소드가 런타임에 생성되는 동적 테스트를위한 테스트 팩토리 |
@TestInstance | 해당 어노테이션이 달린 테스트 클래스에 대한 테스트 인스턴스 수명주기를 구성 |
@TestTemplate | TestTemplateInvocationContextProvider 에 의해 복수 호출되는 테스트 케이스의 템플릿 |
@DisplayName | 테스트 클래스 또는 테스트 메서드에 대한 사용자 지정 표시 이름을 선언하여 표기 |
@BeforeEach | 각 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 메소드보다 먼저 실행되는 어노테이션 (akka @Before in junit4) |
@AfterEach | 각 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 메소드 다음에 실행되는 어노테이션 (@After 와 비슷) |
@BeforeAll | 해당 메소드는 현재 클래스의 모든 @Test, @RepeatedTest, @ParameterizedTest 및 @TestFactory 메소드보다 먼저 실행되어야 함 (@BeforeClass 와 유사) |
@AfterAll | 해당 메소드는 현재 클래스의 모든 @Test, @RepeatedTest, @ParameterizedTest 및 @TestFactory 메소드 다음에 실행되어야 함 (@AfterClass) |
@Nested | 중첩 된 비 정적 테스트 클래스임 |
@Tag | 클래스 또는 메서드 수준에서 필터링 테스트 용 태그를 선언하는 데 사용 |
@Disabled | 테스트 케이스 비활성화 (@Ignore) |
@ExtendWith | 사용자 지정 확장을 등록 |
@Test, @TestTemplate, @RepeatedTest, @BeforeAll, @AfterAll, @BeforeEach 또는 @AfterEach 어노테이션 메소드는 값을 반환하면 안됌.
실제 자주 사용되는 코드들을 통해 샘플을 아래와 같이 작성하여 실행해본다.
package com.libqa.junit;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.fail;
class StandardTests {
@BeforeAll
static void initAll() {
System.out.println("@BeforeAll");
}
@BeforeEach
void init() {
System.out.println("@BeforeEach");
}
@Test
void succeedingTest1() {
System.out.println("Success Test first!!");
}
@Test
void succeedingTest2() {
System.out.println("Success Test second!!");
}
@Test
void failingTest() {
fail("a failing test");
}
@Test
@Disabled("skip this ")
void skippedTest() {
System.out.println("## Disabled for demonstration purposes");
}
@AfterEach
void tearDown() {
System.out.println("@AfterEach");
}
@AfterAll
static void tearDownAll() {
System.out.println("@AfterAll");
}
}
Annotation Discription
에 나온데로, 위 코드를 실행하면 최초 한번과 최종 한번은 @BeforeAll 어노테이션과 @AfterAll이 실행되는 것을 알 수 있다. 또한 이 두 어노테이션은 static 으로 선언 되어야 한다.
succeedingTest1 메소드나 succeedingTest2 각 실행 이전 이후로 @BeforeEach 와 @AfterEach 가 각각 실행된 것을 확인할 수 있다.
실패하는 테스트(failingTest)의 경우 역시 @BeforeEach 와 @AfterEach 가 실행 되었다.
@Disabled 의 경우 skip this 라는 메서드만 실행될 뿐 @BeforeEach 와 @AfterEach 실행되지는 않는다.
실패하는 테스트(failingTest)의 경우 역시 @BeforeEach 와 @AfterEach 가 실행 되었다.
@Disabled 의 경우 skip this 라는 메서드만 실행될 뿐 @BeforeEach 와 @AfterEach 실행되지는 않는다.
@DisplayName("succeedingTest1 was passed")
@Test
void succeedingTest1() {
System.out.println("Success Test first!!");
}
@DisplayName 어노테이션의 경우 JUnit run 실행시 노출되는 명을 지정하 수 있는 어노테이션이다.
Exception
JUnit 5 에서는 주어진 유형의 Exception을 던지는지 확인하는 assertThrows () 메소드가 추가 되었다.
@Test
@DisplayName("Throw 한 익셉션의 메시지를 비교한다.")
void shouldThrowException() {
Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
throw new UnsupportedOperationException("Not supported");
});
assertEquals(exception.getMessage(), "Not supported");
}
실행해보면 정상적으로 테스트를 통과하는 것을 확인할 수 있다.
Tagging
테스트 클래스와 메소드는 @Tag 주석을 통해 태크 마킹을 할 수 있으며 나중에 테스트 검색 및 실행을 필터링하는 데 사용할 수 있다. 이는 계획된 테스트만을 추가하거나, 테스트 실행 계획에서 제외하는 테스트 집합을 만들어 낼 수 있다.
tagging
package를 만들어서 아래와 같은 테스트 케이스를 작성한다.package com.libqa.junit.tagging;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
public class TagTests {
@Test
@Tag("development")
@Tag("production")
void allTestCase() {
System.out.println("Development and Production Test !");
}
@Test
@Tag("development")
void developmentTest() {
System.out.println("Only evelopment Test !");
}
}
development 라는 태그와 production 이라는 태그를 구분해서 메소드에 적용하였다.
build.gradle 에 platform-runner 를 디펜던시에 추가해준다.
apply plugin: 'java'
group 'JUnit5'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
testCompile('org.junit.jupiter:junit-jupiter-api:5.2.0')
testRuntime('org.junit.jupiter:junit-jupiter-engine:5.2.0')
testCompile('org.junit.platform:junit-platform-runner:1.2.0')
}
IncludeTags를 통해 Production 태그만 호출해본다.
package com.libqa.junit.tagging;
import org.junit.Test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;
@RunWith(JUnitPlatform.class)
@SelectPackages("com.libqa.junit.tagging")
@IncludeTags("production")
public class IncludeTagTest {
@Test
void includeProductionTest() {
System.out.println(" 프로젝션 코드가 호출되어야 함");
}
}
TagTests 클래스의
Development and Production Test !
만 호출되는 것을 확인할 수 있다.물론 IncludeTags(“development”) 로 할 경우 development 태그가 적용된 두개의 메소드 모두 호출되는 것을 확인할 수 있으며@ExcludeTags("production")
혹은@IncludeTags({"production","development"})
와 같은 형태로도 사용할 수 있다.
Assertions
JUnit5 의 단정문 (assertions) 은 테스트 케이스의 실제 출력 값을 예상 출력값으로 비교하여 유효성을 확인하는 정적 클래스이다.
assertEquals
, assertNotEquals
, assertArrayEquals
, assertTrue
, assertFalse
등의 구문을 통해 호출된 결과값과 기대값을 비교하는데 사용한다.assertEquals()
기대값과 실제값이 같은지를 비교하는 Assertions.asserEquals() 구문은 아래와 같이 쓰인다.
public static void assertEquals(int expected, int actual)
public static void assertEquals(int expected, int actual, String message)
public static void assertEquals(int expected, int actual, Supplier<String> messageSupplier)
간단한 계산 어플리케이션을 작성해보며 assertEquals를 사용해보자.
com.libqa.junit.assertion 패키지에 CalculatorTest 를 작성한다.
package com.libqa.junit.assertion;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@Test
void addTest() {
long addResult = new Calculator().add(2, 2);
//Test will pass
assertEquals(4L, addResult);
}
}
최초 실행시 Calculator 클래스에 add 메소드가 존재하지 않기 때문에 에러가 발생할 것이다.
add 메소드를 작성한다.
add 메소드를 작성한다.
package com.libqa.junit.assertion;
public class Calculator {
public long add(long first, long second) {
return first + second;
}
}
넘어온 파라미터를 더하여 리턴하는 간단한 메소드로, assertEquals를 통해 타입이나 리턴값을 검증할 수 있다.
다시 테스트케이스를 실행하면 정상적으로 녹색 라인이 출력되는 것을 확인 할 수 있다.
다시 테스트케이스를 실행하면 정상적으로 녹색 라인이 출력되는 것을 확인 할 수 있다.
이제 결과값이 다른 실패 케이스를 작성해보자. 이는, 다른 형태의 메소드 시그니처를 테스트 해보기 위해서 인데, 위의 Assertions.asserEquals() 구문 유형중 두번째와 세번째의 경우를 테스트 해보기 위함이다.
package com.libqa.junit.assertion;
import org.junit.jupiter.api.Test;
import java.util.function.Supplier;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@Test
void addTest() {
long addResult = new Calculator().add(2, 2);
//Test will pass
assertEquals(4L, addResult);
}
@Test
void addFailValueTest() {
long addResult = new Calculator().add(2, 2);
//Test will fail
assertEquals(5L, addResult, "Calculator.add(2, 2) == 5 Is False!!!");
}
@Test
void addFailSupplierTest() {
long addResult = new Calculator().add(2, 2);
//Test will fail
Supplier<String> messageSupplier = () -> "Calculator.add(2, 2) == 3 test failed";
assertEquals(3L, addResult , messageSupplier);
}
}
두번째 메소드인 addFailValueTest 의 경우 assertEquals 를 통해 검증에 실패할 경우 3번째 인자값인 message 가 출력된다.
세번째의 경우도 마찬가지인데, 매개 변수가 있는 좀 더 다양한 방식의 오류 메시지를 작성할 때 쓰인다.
세번째의 경우도 마찬가지인데, 매개 변수가 있는 좀 더 다양한 방식의 오류 메시지를 작성할 때 쓰인다.
assertNotEquals()
마찬가지로 Assertions.assertNotEquals() 의 경우 예상값과 실제값이 동일하지 않다고 단정할 경우 사용된다.
구문은 아래와 같다.
public static void assertNotEquals(Object expected, Object actual)
public static void assertNotEquals(Object expected, Object actual, String message)
public static void assertNotEquals(Object expected, Object actual, Supplier<String> messageSupplier)
Calculator 클래스에 아래와 같이 코드를 완성한 후 테스트를 진행해보자.
package com.libqa.junit.assertion;
public class Calculator {
public long add(long first, long second) {
return first + second;
}
public long subtract(long first, long second) {
return first - second;
}
public long multiply(long first, long second) {
return first * second;
}
public long divide(long first, long second) {
return first / second;
}
}
add, subtract, multiply 의 테스트 케이스를 작성한다.
package com.libqa.junit.assertion;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
class CalculatorNotEqualsTest {
@Test
void addTestNotEqualsGiven3() {
//Test will pass
Long addResult = new Calculator().add(2, 2);
assertNotEquals(3L, addResult);
}
@Test
@DisplayName("4가 주어졌을 때 subtract 결과 값이 같지 않은지 검사한다. ")
void addTestNotEqualsSubtractGiven4() {
Long addResult = new Calculator().subtract(2, 2);
//Test will fail
assertNotEquals(0L, addResult, "addTestNotEqualsSubtractGiven4 test failed");
}
@Test
@DisplayName("Supplier multiply gove x * y Fail ")
void addTestNotEqualsSupplierGiven4() {
int x = 3;
int y = 5;
Long expected = 15L;
Long addResult = new Calculator().multiply(x, y);
assertNotEquals(expected, addResult,
() -> String.format("%s * %s != %s", x, y, expected));
}
}
addTestNotEqualsGiven3
테스트는 기대값과 같지 않기 때문에 정상적으로 통과 하지만 나머지 두개의 테스트는 기대값과 같기 때문에 assertNotEquals 테스트를 통과 하지 못한다.addTestNotEqualsSubtractGiven4 의 에러 메시지
addTestNotEqualsSupplierGiven4 의 에러 메시지
assertArrayEquals()
Array 의 경우 assertArrayEquals() 로 실제값의 어레이 값을 단정할 수 있다. 마찬가지로 테스트 통과 실패시 출력할 메시지를 지원한다.
구문은 아래와 같다.
구문은 아래와 같다.
public static void assertArrayEquals(int[] expected, int[] actual)
public static void assertArrayEquals(int[] expected, int[] actual, String message)
public static void assertArrayEquals(int[] expected, int[] actual, Supplier<String> messageSupplier)
예제를 살펴보자.
package com.libqa.junit.assertion;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
public class AssertArrayEqualsTest {
@Test
void assertArraysTest() {
int[] one = {1, 2, 3};
int[] two = {1, 2, 3};
assertArrayEquals(one, two, "ArrayEquals Test");
}
@Test
void assertArraysWrongTest() {
int[] one = {1, 2, 3};
int[] two = {1, 2, 3, 4};
assertArrayEquals(one, two, "ArrayEquals assertArraysWrongTest Test");
}
}
assertIterableEquals()
assertIterableEquals() 는 예상과 실제 iterable 의 요소 수와 순서가 동일함을 테스트 한다.
구문은 아래와 같다.
구문은 아래와 같다.
public static void assertIterableEquals(Iterable<?> expected, Iterable> actual)
public static void assertIterableEquals(Iterable<?> expected, Iterable> actual, String message)
public static void assertIterableEquals(Iterable<?> expected, Iterable> actual, Supplier<String> messageSupplier)
package com.libqa.junit.assertion;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
public class AssertIterableEquals {
@Test
void assertIterableEqualsTest() {
Iterable<Integer> listOne = new ArrayList<>(Arrays.asList(1,2,3,4));
Iterable<Integer> listTwo = new ArrayList<>(Arrays.asList(1,2,3,4));
Iterable<Integer> listThree = new ArrayList<>(Arrays.asList(1,2,3));
assertIterableEquals(listOne, listTwo);
//assertIterableEquals(listOne, listThree, "Iterable List not matched");
}
}
assertNotNull & assertNull
assertNotNull()은 actual가 null이 아니라고 단정한다.
마찬가지로 assertNull() 메소드는 actual가 null이라는 것을 나타낸다.
마찬가지로 assertNull() 메소드는 actual가 null이라는 것을 나타낸다.
public static void assertNotNull(Object actual)
public static void assertNotNull(Object actual, String message)
public static void assertNotNull(Object actual, Supplier<String> messageSupplier)
public static void assertNull(Object actual)
public static void assertNull(Object actual, String message)
public static void assertNull(Object actual, Supplier<String> messageSupplier)
assertNotSame && assertSane
assertNotSame()은 예상과 실제가 동일한 객체를 참조하지 않는다고 단정한다.
마찬가지로 assertSame() 메소드는 예상과 실제가 동일한 객체를 참조한다고 단정한다. 구문은 아래와 같다.
마찬가지로 assertSame() 메소드는 예상과 실제가 동일한 객체를 참조한다고 단정한다. 구문은 아래와 같다.
public static void assertNotSame(Object actual)
public static void assertNotSame(Object actual, String message)
public static void assertNotSame(Object actual, Supplier<> messageSupplier)
public static void assertSame(Object actual)
public static void assertSame(Object actual, String message)
public static void assertSame(Object actual, Supplier<String> messageSupplier)
assertTrue & assertFalse
assertTrue() 는 제공된 조건이 true이거나 BooleanSupplier가 제공하는 조건이 true임을 단정한다.
마찬가지로 assertFalse() 는 제공된 조건이 거짓임을 나타낸다. 구문은 아래와 같다.
마찬가지로 assertFalse() 는 제공된 조건이 거짓임을 나타낸다. 구문은 아래와 같다.
public static void assertTrue(boolean condition)
public static void assertTrue(boolean condition, String message)
public static void assertTrue(boolean condition, Supplier<String> messageSupplier)
public static void assertTrue(BooleanSupplier booleanSupplier)
public static void assertTrue(BooleanSupplier booleanSupplier, String message)
public static void assertTrue(BooleanSupplier booleanSupplier, Supplier<String> messageSupplier)
public static void assertFalse(boolean condition)
public static void assertFalse(boolean condition, String message)
public static void assertFalse(boolean condition, Supplier<String> messageSupplier)
public static void assertFalse(BooleanSupplier booleanSupplier)
public static void assertFalse(BooleanSupplier booleanSupplier, String message)
public static void assertFalse(BooleanSupplier booleanSupplier, Supplier<String> messageSupplier)
예제 코드는 아래와 같다.
package com.libqa.junit.assertion;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class AssertTrueTest {
private static String getMessage () {
return "Test result is False";
}
private static boolean getResult () {
return true;
}
@Test
void assertTrueTest() {
assertTrue(AssertTrueTest::getResult, AssertTrueTest::getMessage);
}
@Test
void assertFalseTest() {
assertFalse(AssertTrueTest::getResult, AssertTrueTest::getMessage);
}
}
assertThrows
테스트중인 어플리케이션에서 던져진 예외에 대한 단정은 assertThrows() 메소드를 통해 확인이 가능하다.
assertThrows() 메소드는 다음의 매개변수들을 사용한다.
assertThrows() 메소드는 다음의 매개변수들을 사용한다.
- 예상되는 예외의 형태를 지정하는 클래스 객체
- 테스트중인 어플리케이션을 호출하는 Executable 객체
- 선택적 오류 메시지
기본적인 구문은 아래와 같다.
assertThrows(Class<T> expectedType, Executable executable)
assertThrows(Class<T> expectedType, Executable executable, String message)
assertThrows(Class<T> expectedType, Executable executable, Supplier<String> messageSupplier)
NullPointer Exception을 던지는 테스트 케이스를 가졍하고 assertThrows를 사용항면 아래와 같다.
package com.libqa.junit.assertion;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DisplayName("Writing assertions for exceptions")
class ExceptionAssertionTest {
@Test
@DisplayName("Should throw the NullPointerException")
void shouldThrowCorrectException() {
assertThrows(
NullPointerException.class,
() -> { throw new NullPointerException(); }
);
}
@Test
@DisplayName("Should throw an exception that has the correct message")
void shouldThrowAnExceptionWithCorrectMessage() {
final NullPointerException thrown = assertThrows(
NullPointerException.class,
() -> { throw new NullPointerException("Hello World!"); }
);
assertEquals("Hello World!", thrown.getMessage());
}
}
발생 된 예외에 올바른 메시지가 있는지 확인하려면 shouldThrowAnExceptionWithCorrectMessage() 과 같이 작성할 수 있다.
Assumptions
Assumptions 클래스는 가정을 기반으로 조건부 테스트 실행을 지원하는 정적 메서드를 제공한다. 실패할 경우 테스트가 중단된다.
assumTrue
assumTrue() 는 주어진 가정을 true로 가정하고 가정이 true이면 테스트가 진행되고, 그렇지 않으면 테스트 실행을 중단한다.
기본적인 구문은 아래와 같다.
public static void assumeTrue(boolean assumption) throws TestAbortedException
public static void assumeTrue(boolean assumption, Supplier<String> messageSupplier) throws TestAbortedException
public static void assumeTrue(boolean assumption, String message) throws TestAbortedException
public static void assumeTrue(BooleanSupplier assumptionSupplier) throws TestAbortedException
public static void assumeTrue(BooleanSupplier assumptionSupplier, String message) throws TestAbortedException
public static void assumeTrue(BooleanSupplier assumptionSupplier, Supplier<String> messageSupplier) throws TestAbortedException
예제를 살펴보자.
package com.libqa.junit.assumption;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class AssumeTrue {
private static String message() {
return "TEST Execution Failed ";
}
@Test
void systemIsDevelopTest() {
System.setProperty("ENV", "DEV");
assumeTrue("DEV".equals(System.getProperty("ENV")));
//remainder of test will proceed
assertEquals(System.getProperty("ENV"), "PROD", "프로덕션 환경이 아닙니다.");
}
@Test
void systemIsProductionTest() {
System.setProperty("ENV", "PROD");
assumeTrue("DEV".equals(System.getProperty("ENV")), AssumeTrue::message);
//remainder of test will be aborted
assertEquals(System.getProperty("ENV"), "DEV", "개발 환경이 아닙니다.");
}
}
systemIsDevelopTest() 메소드의 경우 assumTrue 에 의해 결과가 true가 되고 다음 테스트 구문이 실행이 되는 것을 확인 할 수 있으나 systemIsProductionTest() 의 경우 assumTrue 이후 결과가 false 임에 따라 다음 테스트 구문은 실행이 되지 않는것을 확인할 수 있다.
SelectClasses에 의한 Test Suite 만들기
Single class일 경우 아래와 같이 @SelectClasses 어노테이션에 하나의 클래스를 파라미터로 전달함으로써 테스트 할 수 있다.
@RunWith(JUnitPlatform.class)
@SelectClasses( ClassATest.class )
public class JUnit5TestSuiteExample {
...
}
여러 클래스를 지정해야 할 경우 Array 형태의
{ a.class , b.class, ...}
를 @SelectClasses 어노테이션에 전달함으로써 테스트를 할 수 있다.@RunWith(JUnitPlatform.class)
@SelectClasses( { ClassATest.class, ClassBTest.class, ClassCTest.class } )
public class JUnit5TestSuiteExample {
...
}
특정 하위 패키지를 제외하거나 패키지를 포함 시키려면 @IncludePackages 및 @ExcludePackages Annotation을 활용하여 @SelectPackages 하위에 추가하거나 제외할 수 있다. (@IncludeTags 와 @ExcludeTags 도 사용 가능함)
이와 같이 다양한 형태의 Test Suite 를 작성할 수 있으며 필터링을 통해 복합적인 테스트가 가능해졌다.
댓글 없음:
댓글 쓰기